@famgia/omnify-ai-guides 2.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -0
- package/dist/chunk-RCTEXK7C.js +549 -0
- package/dist/chunk-RCTEXK7C.js.map +1 -0
- package/dist/config/rules.yaml +524 -0
- package/dist/index.cjs +587 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +55 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge/agents/architect.md.stub +150 -0
- package/dist/knowledge/agents/developer.md.stub +190 -0
- package/dist/knowledge/agents/reviewer.md.stub +134 -0
- package/dist/knowledge/agents/tester.md.stub +196 -0
- package/dist/knowledge/checklists/backend.md.stub +112 -0
- package/dist/knowledge/checklists/react.md.stub +108 -0
- package/dist/knowledge/claude-rules/laravel-controllers.md.stub +57 -0
- package/dist/knowledge/claude-rules/laravel-migrations.md.stub +47 -0
- package/dist/knowledge/claude-rules/laravel-tests.md.stub +52 -0
- package/dist/knowledge/claude-rules/naming.md.stub +369 -0
- package/dist/knowledge/claude-rules/performance.md.stub +256 -0
- package/dist/knowledge/claude-rules/php-standards.md.stub +305 -0
- package/dist/knowledge/claude-rules/react-components.md.stub +67 -0
- package/dist/knowledge/claude-rules/schema-yaml.md.stub +83 -0
- package/dist/knowledge/claude-rules/security.md.stub +164 -0
- package/dist/knowledge/cursor-rules/antd-deprecations.mdc.stub +62 -0
- package/dist/knowledge/cursor-rules/basemodel-readonly.mdc.stub +66 -0
- package/dist/knowledge/cursor-rules/baserequest-readonly.mdc.stub +74 -0
- package/dist/knowledge/cursor-rules/baseresource-readonly.mdc.stub +78 -0
- package/dist/knowledge/cursor-rules/laravel-controller.mdc.stub +421 -0
- package/dist/knowledge/cursor-rules/laravel-request.mdc.stub +112 -0
- package/dist/knowledge/cursor-rules/laravel-resource.mdc.stub +73 -0
- package/dist/knowledge/cursor-rules/laravel-review.mdc.stub +69 -0
- package/dist/knowledge/cursor-rules/laravel-testing.mdc.stub +138 -0
- package/dist/knowledge/cursor-rules/laravel.mdc.stub +138 -0
- package/dist/knowledge/cursor-rules/migrations-workflow.mdc.stub +224 -0
- package/dist/knowledge/cursor-rules/model-editable.mdc.stub +120 -0
- package/dist/knowledge/cursor-rules/omnify-migrations.mdc.stub +109 -0
- package/dist/knowledge/cursor-rules/omnify-schema.mdc.stub +358 -0
- package/dist/knowledge/cursor-rules/omnify.mdc.stub +58 -0
- package/dist/knowledge/cursor-rules/react-design.mdc.stub +693 -0
- package/dist/knowledge/cursor-rules/react-form.mdc.stub +292 -0
- package/dist/knowledge/cursor-rules/react-services.mdc.stub +304 -0
- package/dist/knowledge/cursor-rules/react.mdc.stub +336 -0
- package/dist/knowledge/cursor-rules/request-editable.mdc.stub +111 -0
- package/dist/knowledge/cursor-rules/resource-editable.mdc.stub +125 -0
- package/dist/knowledge/cursor-rules/schema-create.mdc.stub +440 -0
- package/dist/knowledge/cursor-rules/validation-rules.mdc.stub +181 -0
- package/dist/knowledge/laravel/README.md.stub +59 -0
- package/dist/knowledge/laravel/architecture.md.stub +424 -0
- package/dist/knowledge/laravel/authentication.md.stub +588 -0
- package/dist/knowledge/laravel/controller.md.stub +484 -0
- package/dist/knowledge/laravel/datetime.md.stub +334 -0
- package/dist/knowledge/laravel/migrations-team.md.stub +376 -0
- package/dist/knowledge/laravel/openapi.md.stub +449 -0
- package/dist/knowledge/laravel/request.md.stub +450 -0
- package/dist/knowledge/laravel/resource.md.stub +516 -0
- package/dist/knowledge/laravel/service.md.stub +503 -0
- package/dist/knowledge/laravel/testing.md.stub +1504 -0
- package/dist/knowledge/omnify/antdesign-guide.md.stub +401 -0
- package/dist/knowledge/omnify/config-guide.md.stub +405 -0
- package/dist/knowledge/omnify/japan-guide.md.stub +186 -0
- package/dist/knowledge/omnify/laravel-guide.md.stub +61 -0
- package/dist/knowledge/omnify/partial-schema-guide.md.stub +353 -0
- package/dist/knowledge/omnify/react-form-guide.md.stub +225 -0
- package/dist/knowledge/omnify/schema-guide.md.stub +144 -0
- package/dist/knowledge/omnify/typescript-guide.md.stub +337 -0
- package/dist/knowledge/react/README.md.stub +221 -0
- package/dist/knowledge/react/antd-guide.md +528 -0
- package/dist/knowledge/react/antd-guide.md.stub +528 -0
- package/dist/knowledge/react/checklist.md.stub +108 -0
- package/dist/knowledge/react/datetime-guide.md.stub +137 -0
- package/dist/knowledge/react/design-philosophy.md.stub +363 -0
- package/dist/knowledge/react/i18n-guide.md.stub +211 -0
- package/dist/knowledge/react/laravel-integration.md.stub +181 -0
- package/dist/knowledge/react/service-pattern.md.stub +180 -0
- package/dist/knowledge/react/tanstack-query.md.stub +339 -0
- package/dist/knowledge/react/types-guide.md +669 -0
- package/dist/knowledge/react/types-guide.md.stub +669 -0
- package/dist/knowledge/workflows/bug-fix.md.stub +201 -0
- package/dist/knowledge/workflows/code-review.md.stub +164 -0
- package/dist/knowledge/workflows/new-feature.md.stub +327 -0
- package/dist/plugin-M95GyBll.d.cts +191 -0
- package/dist/plugin-M95GyBll.d.ts +191 -0
- package/dist/plugin.cjs +573 -0
- package/dist/plugin.cjs.map +1 -0
- package/dist/plugin.d.cts +2 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +15 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Laravel Integration
|
|
2
|
+
|
|
3
|
+
> **Related:** [README](./README.md) | [Service Pattern](./service-pattern.md)
|
|
4
|
+
|
|
5
|
+
## Sanctum Authentication Flow
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// Step 1: Get CSRF cookie (required before POST requests)
|
|
9
|
+
await api.get("/sanctum/csrf-cookie");
|
|
10
|
+
|
|
11
|
+
// Step 2: Login
|
|
12
|
+
await api.post("/login", { email, password });
|
|
13
|
+
// Cookie is now set automatically
|
|
14
|
+
|
|
15
|
+
// Step 3: Access protected routes
|
|
16
|
+
await api.get("/api/user"); // Works! Cookie sent automatically
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Error Handling Map
|
|
22
|
+
|
|
23
|
+
| HTTP Status | Laravel Meaning | Frontend Action |
|
|
24
|
+
| ----------- | ------------------- | ------------------------- |
|
|
25
|
+
| 200 | Success | Process response |
|
|
26
|
+
| 201 | Created | Process response |
|
|
27
|
+
| 204 | No Content | Success (no body) |
|
|
28
|
+
| 401 | Unauthenticated | Redirect to `/login` |
|
|
29
|
+
| 403 | Forbidden | Show error message |
|
|
30
|
+
| 404 | Not Found | Show error message |
|
|
31
|
+
| 419 | CSRF Token Mismatch | Refresh page |
|
|
32
|
+
| 422 | Validation Error | Display in form fields |
|
|
33
|
+
| 429 | Too Many Requests | Show rate limit message |
|
|
34
|
+
| 500+ | Server Error | Show server error message |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## API Response Types
|
|
39
|
+
|
|
40
|
+
### Laravel Pagination Response
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
interface PaginatedResponse<T> {
|
|
44
|
+
data: T[];
|
|
45
|
+
links: {
|
|
46
|
+
first: string | null;
|
|
47
|
+
last: string | null;
|
|
48
|
+
prev: string | null;
|
|
49
|
+
next: string | null;
|
|
50
|
+
};
|
|
51
|
+
meta: {
|
|
52
|
+
current_page: number;
|
|
53
|
+
from: number | null;
|
|
54
|
+
last_page: number;
|
|
55
|
+
per_page: number;
|
|
56
|
+
to: number | null;
|
|
57
|
+
total: number;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Laravel API Resource Response (single item)
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
interface ResourceResponse<T> {
|
|
66
|
+
data: T;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Laravel Validation Error Response (422)
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
interface ValidationErrorResponse {
|
|
74
|
+
message: string;
|
|
75
|
+
errors: Record<string, string[]>;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Handling Laravel Responses
|
|
82
|
+
|
|
83
|
+
### In Service Layer
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const userService = {
|
|
87
|
+
// Paginated list
|
|
88
|
+
list: async (params?: UserListParams): Promise<PaginatedResponse<User>> => {
|
|
89
|
+
const { data } = await api.get("/api/users", { params });
|
|
90
|
+
return data; // Already typed as PaginatedResponse
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Single resource (handle { data: ... } wrapper)
|
|
94
|
+
get: async (id: number): Promise<User> => {
|
|
95
|
+
const { data } = await api.get(`/api/users/${id}`);
|
|
96
|
+
return data.data ?? data; // Handle both wrapped and unwrapped
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### In Components (Form Validation)
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { getFormErrors } from "@/lib/api";
|
|
105
|
+
|
|
106
|
+
const mutation = useMutation({
|
|
107
|
+
mutationFn: userService.create,
|
|
108
|
+
onError: (error) => {
|
|
109
|
+
// Transform Laravel 422 errors to Ant Design format
|
|
110
|
+
form.setFields(getFormErrors(error));
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Axios Instance Configuration
|
|
118
|
+
|
|
119
|
+
The `lib/api.ts` is pre-configured for Laravel Sanctum:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
const api = axios.create({
|
|
123
|
+
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
|
124
|
+
timeout: 30000,
|
|
125
|
+
headers: {
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
Accept: "application/json",
|
|
128
|
+
},
|
|
129
|
+
withCredentials: true, // Required for Sanctum cookies
|
|
130
|
+
withXSRFToken: true, // Auto send XSRF-TOKEN cookie as header
|
|
131
|
+
xsrfCookieName: "XSRF-TOKEN",
|
|
132
|
+
xsrfHeaderName: "X-XSRF-TOKEN",
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Common Patterns
|
|
139
|
+
|
|
140
|
+
### Login Flow
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { csrf } from "@/lib/api";
|
|
144
|
+
|
|
145
|
+
const login = async (email: string, password: string) => {
|
|
146
|
+
// 1. Get CSRF cookie first
|
|
147
|
+
await csrf();
|
|
148
|
+
|
|
149
|
+
// 2. Login
|
|
150
|
+
await api.post("/login", { email, password });
|
|
151
|
+
|
|
152
|
+
// 3. Get user data
|
|
153
|
+
const { data } = await api.get("/api/user");
|
|
154
|
+
return data;
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Protected API Call
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// No special handling needed - cookies are sent automatically
|
|
162
|
+
const users = await userService.list();
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Handling 401 (Unauthenticated)
|
|
166
|
+
|
|
167
|
+
The interceptor in `lib/api.ts` automatically redirects to `/login` on 401:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
api.interceptors.response.use(
|
|
171
|
+
(response) => response,
|
|
172
|
+
(error) => {
|
|
173
|
+
if (error.response?.status === 401) {
|
|
174
|
+
if (!window.location.pathname.includes("/login")) {
|
|
175
|
+
window.location.href = "/login";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return Promise.reject(error);
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
```
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Service Layer Pattern
|
|
2
|
+
|
|
3
|
+
> **Related:** [README](./README.md) | [TanStack Query](./tanstack-query.md) | [Laravel Integration](./laravel-integration.md)
|
|
4
|
+
|
|
5
|
+
## Types Rule
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// ✅ DO: Import Model + Create/Update types from Omnify
|
|
9
|
+
import type { User, UserCreate, UserUpdate } from "@omnify/schemas";
|
|
10
|
+
|
|
11
|
+
// ✅ DO: Only define query params locally (not generated by Omnify)
|
|
12
|
+
export interface UserListParams {
|
|
13
|
+
search?: string;
|
|
14
|
+
page?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ❌ DON'T: Define types that Omnify already generates
|
|
18
|
+
export interface UserCreateInput { ... } // WRONG - use UserCreate from Omnify
|
|
19
|
+
export interface User { ... } // WRONG - use User from Omnify
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Service Template
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
/**
|
|
28
|
+
* Users Service
|
|
29
|
+
*
|
|
30
|
+
* CRUD operations for User resource.
|
|
31
|
+
* Maps to Laravel UserController.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import api, { PaginatedResponse } from "@/lib/api";
|
|
35
|
+
import type { User, UserCreate, UserUpdate } from "@omnify/schemas";
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// Types - Only query params (Create/Update come from Omnify)
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
/** Query params for listing users */
|
|
42
|
+
export interface UserListParams {
|
|
43
|
+
search?: string;
|
|
44
|
+
page?: number;
|
|
45
|
+
per_page?: number;
|
|
46
|
+
sort_by?: keyof User;
|
|
47
|
+
sort_order?: "asc" | "desc";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Service
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
const BASE_URL = "/api/users";
|
|
55
|
+
|
|
56
|
+
export const userService = {
|
|
57
|
+
/**
|
|
58
|
+
* Get paginated list of users
|
|
59
|
+
* GET /api/users
|
|
60
|
+
*/
|
|
61
|
+
list: async (params?: UserListParams): Promise<PaginatedResponse<User>> => {
|
|
62
|
+
const { data } = await api.get(BASE_URL, { params });
|
|
63
|
+
return data;
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get single user by ID
|
|
68
|
+
* GET /api/users/:id
|
|
69
|
+
*/
|
|
70
|
+
get: async (id: number): Promise<User> => {
|
|
71
|
+
const { data } = await api.get(`${BASE_URL}/${id}`);
|
|
72
|
+
return data.data ?? data;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create new user
|
|
77
|
+
* POST /api/users
|
|
78
|
+
*/
|
|
79
|
+
create: async (input: UserCreate): Promise<User> => {
|
|
80
|
+
const { data } = await api.post(BASE_URL, input);
|
|
81
|
+
return data.data ?? data;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Update existing user
|
|
86
|
+
* PUT /api/users/:id
|
|
87
|
+
*/
|
|
88
|
+
update: async (id: number, input: UserUpdate): Promise<User> => {
|
|
89
|
+
const { data } = await api.put(`${BASE_URL}/${id}`, input);
|
|
90
|
+
return data.data ?? data;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Delete user
|
|
95
|
+
* DELETE /api/users/:id
|
|
96
|
+
*/
|
|
97
|
+
delete: async (id: number): Promise<void> => {
|
|
98
|
+
await api.delete(`${BASE_URL}/${id}`);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Service Rules
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// ✅ DO: Import all types from Omnify
|
|
109
|
+
import type { User, UserCreate, UserUpdate } from "@omnify/schemas";
|
|
110
|
+
|
|
111
|
+
// ❌ DON'T: Define types that Omnify generates
|
|
112
|
+
export interface UserCreate { ... } // WRONG!
|
|
113
|
+
export interface User { ... } // WRONG!
|
|
114
|
+
|
|
115
|
+
// ✅ DO: Only define query params locally
|
|
116
|
+
export interface UserListParams { ... } // OK - not in Omnify
|
|
117
|
+
|
|
118
|
+
// ✅ DO: Keep services pure (no React hooks)
|
|
119
|
+
export const userService = {
|
|
120
|
+
get: (id) => api.get(`/api/users/${id}`).then(r => r.data.data ?? r.data),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ❌ DON'T: Use React hooks in services
|
|
124
|
+
export const userService = {
|
|
125
|
+
get: (id) => {
|
|
126
|
+
const [data, setData] = useState(); // WRONG!
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// ✅ DO: Handle Laravel's { data: ... } wrapper
|
|
131
|
+
get: (id) => api.get(`/api/users/${id}`).then(r => r.data.data ?? r.data),
|
|
132
|
+
|
|
133
|
+
// ❌ DON'T: Return raw axios response
|
|
134
|
+
get: (id) => api.get(`/api/users/${id}`), // Returns AxiosResponse, not data
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Using Validation Rules
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { Form, Input } from "antd";
|
|
143
|
+
import { useLocale } from "next-intl";
|
|
144
|
+
import { userSchemas, getUserFieldLabel } from "@omnify/schemas/User";
|
|
145
|
+
import { zodRule } from "@/lib/form-validation";
|
|
146
|
+
|
|
147
|
+
function UserForm() {
|
|
148
|
+
const locale = useLocale();
|
|
149
|
+
const label = (key: string) => getUserFieldLabel(key, locale);
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<Form>
|
|
153
|
+
{/* Name */}
|
|
154
|
+
<Form.Item
|
|
155
|
+
name="name"
|
|
156
|
+
label={label("name")}
|
|
157
|
+
rules={[zodRule(userSchemas.name, label("name"))]}
|
|
158
|
+
>
|
|
159
|
+
<Input />
|
|
160
|
+
</Form.Item>
|
|
161
|
+
</Form>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Types Summary
|
|
169
|
+
|
|
170
|
+
| Type | Source | Example |
|
|
171
|
+
| -------- | ---------------------------- | ------------------------------------------ |
|
|
172
|
+
| Model | `@omnify/schemas` (Omnify) | `User` |
|
|
173
|
+
| Create | `@omnify/schemas` (Omnify) | `UserCreate` |
|
|
174
|
+
| Update | `@omnify/schemas` (Omnify) | `UserUpdate` |
|
|
175
|
+
| Schemas | `@omnify/schemas` (Omnify) | `userSchemas.email` |
|
|
176
|
+
| Params | Service file (manual) | `UserListParams` |
|
|
177
|
+
| Response | `@/lib/api.ts` | `PaginatedResponse<T>` |
|
|
178
|
+
| Rules | `@/lib/form-validation` | `zodRule(schema, label)` |
|
|
179
|
+
|
|
180
|
+
See [Types Guide](./types-guide.md) for complete details.
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# TanStack Query Guide
|
|
2
|
+
|
|
3
|
+
> **Related:** [README](./README.md) | [Service Pattern](./service-pattern.md)
|
|
4
|
+
|
|
5
|
+
## Query Keys Pattern
|
|
6
|
+
|
|
7
|
+
### Structure
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// lib/queryKeys.ts
|
|
11
|
+
|
|
12
|
+
import type { UserListParams } from "@/services/users"; // Import from service
|
|
13
|
+
import type { PostListParams } from "@/services/posts";
|
|
14
|
+
|
|
15
|
+
export const queryKeys = {
|
|
16
|
+
// Simple key
|
|
17
|
+
user: ["user"] as const,
|
|
18
|
+
|
|
19
|
+
// Resource with nested keys - USE TYPED PARAMS
|
|
20
|
+
users: {
|
|
21
|
+
all: ["users"] as const,
|
|
22
|
+
lists: () => [...queryKeys.users.all, "list"] as const,
|
|
23
|
+
list: (params?: UserListParams) => [...queryKeys.users.lists(), params] as const,
|
|
24
|
+
details: () => [...queryKeys.users.all, "detail"] as const,
|
|
25
|
+
detail: (id: number) => [...queryKeys.users.details(), id] as const,
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
posts: {
|
|
29
|
+
all: ["posts"] as const,
|
|
30
|
+
lists: () => [...queryKeys.posts.all, "list"] as const,
|
|
31
|
+
list: (params?: PostListParams) => [...queryKeys.posts.lists(), params] as const,
|
|
32
|
+
details: () => [...queryKeys.posts.all, "detail"] as const,
|
|
33
|
+
detail: (id: number) => [...queryKeys.posts.details(), id] as const,
|
|
34
|
+
byUser: (userId: number) => [...queryKeys.posts.all, "user", userId] as const,
|
|
35
|
+
},
|
|
36
|
+
} as const;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### ⚠️ Type Rule
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// ✅ DO: Import and use specific types from service
|
|
43
|
+
import type { UserListParams } from "@/services/users";
|
|
44
|
+
list: (params?: UserListParams) => [...]
|
|
45
|
+
|
|
46
|
+
// ❌ DON'T: Use generic Record type
|
|
47
|
+
list: (params?: Record<string, unknown>) => [...] // Hard to read, no autocomplete
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Rules
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// ✅ DO: Use query key factory
|
|
54
|
+
useQuery({
|
|
55
|
+
queryKey: queryKeys.users.detail(id),
|
|
56
|
+
queryFn: () => userService.get(id),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ❌ DON'T: Hardcode query keys
|
|
60
|
+
useQuery({
|
|
61
|
+
queryKey: ["users", "detail", id], // Hard to maintain
|
|
62
|
+
queryFn: () => userService.get(id),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ✅ DO: Include all dependencies in key
|
|
66
|
+
useQuery({
|
|
67
|
+
queryKey: queryKeys.users.list({ page, search }), // Refetches when params change
|
|
68
|
+
queryFn: () => userService.list({ page, search }),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ❌ DON'T: Omit dependencies
|
|
72
|
+
useQuery({
|
|
73
|
+
queryKey: queryKeys.users.list(), // Won't refetch when params change
|
|
74
|
+
queryFn: () => userService.list({ page, search }),
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Mutation Pattern
|
|
81
|
+
|
|
82
|
+
### Standard CRUD Mutations
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
"use client";
|
|
86
|
+
|
|
87
|
+
import { Form, Button } from "antd";
|
|
88
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
89
|
+
import { message } from "antd";
|
|
90
|
+
import { useRouter } from "next/navigation";
|
|
91
|
+
import { useTranslations } from "next-intl";
|
|
92
|
+
import { queryKeys } from "@/lib/queryKeys";
|
|
93
|
+
import { getFormErrors } from "@/lib/api";
|
|
94
|
+
import { userService, UserUpdateInput } from "@/services/users";
|
|
95
|
+
|
|
96
|
+
export default function CreateUserPage() {
|
|
97
|
+
const t = useTranslations(); // No namespace = access all
|
|
98
|
+
const router = useRouter();
|
|
99
|
+
const queryClient = useQueryClient();
|
|
100
|
+
const [form] = Form.useForm();
|
|
101
|
+
|
|
102
|
+
// CREATE mutation
|
|
103
|
+
const createMutation = useMutation({
|
|
104
|
+
mutationFn: userService.create,
|
|
105
|
+
onSuccess: () => {
|
|
106
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
|
|
107
|
+
message.success(t("messages.created"));
|
|
108
|
+
router.push("/users");
|
|
109
|
+
},
|
|
110
|
+
onError: (error) => {
|
|
111
|
+
form.setFields(getFormErrors(error));
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// UPDATE mutation
|
|
116
|
+
const updateMutation = useMutation({
|
|
117
|
+
mutationFn: ({ id, data }: { id: number; data: UserUpdateInput }) =>
|
|
118
|
+
userService.update(id, data),
|
|
119
|
+
onSuccess: (_, { id }) => {
|
|
120
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
|
|
121
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.detail(id) });
|
|
122
|
+
message.success(t("messages.updated"));
|
|
123
|
+
router.push(`/users/${id}`);
|
|
124
|
+
},
|
|
125
|
+
onError: (error) => {
|
|
126
|
+
form.setFields(getFormErrors(error));
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// DELETE mutation
|
|
131
|
+
const deleteMutation = useMutation({
|
|
132
|
+
mutationFn: userService.delete,
|
|
133
|
+
onSuccess: () => {
|
|
134
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
|
|
135
|
+
message.success(t("messages.deleted"));
|
|
136
|
+
router.push("/users");
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Form form={form} onFinish={(values) => createMutation.mutate(values)}>
|
|
142
|
+
{/* ... form fields ... */}
|
|
143
|
+
<Button type="primary" htmlType="submit" loading={createMutation.isPending}>
|
|
144
|
+
{t("common.save")}
|
|
145
|
+
</Button>
|
|
146
|
+
</Form>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Mutation Rules
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// ✅ DO: Always invalidate related queries after mutation
|
|
155
|
+
onSuccess: () => {
|
|
156
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ❌ DON'T: Forget to invalidate
|
|
160
|
+
onSuccess: () => {
|
|
161
|
+
message.success("Saved!"); // Data won't refresh!
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ✅ DO: Handle form errors from Laravel
|
|
165
|
+
onError: (error) => {
|
|
166
|
+
form.setFields(getFormErrors(error));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ❌ DON'T: Ignore errors
|
|
170
|
+
onError: (error) => {
|
|
171
|
+
console.log(error); // User sees nothing
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ✅ DO: Show loading state
|
|
175
|
+
<Button loading={mutation.isPending}>Submit</Button>
|
|
176
|
+
|
|
177
|
+
// ❌ DON'T: No loading feedback
|
|
178
|
+
<Button>Submit</Button> // User can double-click
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Advanced Tips
|
|
184
|
+
|
|
185
|
+
### Keep Queries Simple
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// ✅ SIMPLE: queryFn just calls service
|
|
189
|
+
useQuery({
|
|
190
|
+
queryKey: queryKeys.users.list(filters),
|
|
191
|
+
queryFn: () => userService.list(filters),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ❌ OVER-ENGINEERED: Logic in queryFn
|
|
195
|
+
useQuery({
|
|
196
|
+
queryKey: queryKeys.users.list(filters),
|
|
197
|
+
queryFn: async () => {
|
|
198
|
+
const data = await userService.list(filters);
|
|
199
|
+
return data.map(transform).filter(validate); // Move this to service!
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Query Key = All Dependencies
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// ✅ CORRECT: Key includes all params → auto refetch when changed
|
|
208
|
+
useQuery({
|
|
209
|
+
queryKey: queryKeys.users.list({ page, search, status }),
|
|
210
|
+
queryFn: () => userService.list({ page, search, status }),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ❌ WRONG: Missing deps in key → stale data
|
|
214
|
+
useQuery({
|
|
215
|
+
queryKey: queryKeys.users.list(),
|
|
216
|
+
queryFn: () => userService.list({ page, search }), // Params not in key!
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Conditional Queries with `enabled`
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// Fetch user first, then fetch user's posts
|
|
224
|
+
const { data: user } = useQuery({
|
|
225
|
+
queryKey: queryKeys.user,
|
|
226
|
+
queryFn: authService.me,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const { data: posts } = useQuery({
|
|
230
|
+
queryKey: queryKeys.posts.byUser(user?.id!),
|
|
231
|
+
queryFn: () => postService.listByUser(user!.id),
|
|
232
|
+
enabled: !!user, // ← Only runs when user exists
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Invalidate Correctly
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// ✅ Invalidate by prefix (all user queries)
|
|
240
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
|
|
241
|
+
|
|
242
|
+
// ✅ Invalidate specific query
|
|
243
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.detail(id) });
|
|
244
|
+
|
|
245
|
+
// ❌ DON'T invalidate everything
|
|
246
|
+
queryClient.invalidateQueries(); // Too broad!
|
|
247
|
+
|
|
248
|
+
// ❌ DON'T refetch manually
|
|
249
|
+
await userService.create(data);
|
|
250
|
+
refetch(); // Wrong! Use invalidateQueries
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Optimistic Updates (Use Sparingly)
|
|
254
|
+
|
|
255
|
+
Only for instant feedback UX (like/unlike, toggle, drag-drop):
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
const likeMutation = useMutation({
|
|
259
|
+
mutationFn: postService.like,
|
|
260
|
+
onMutate: async (postId) => {
|
|
261
|
+
await queryClient.cancelQueries({ queryKey: queryKeys.posts.detail(postId) });
|
|
262
|
+
const previous = queryClient.getQueryData(queryKeys.posts.detail(postId));
|
|
263
|
+
queryClient.setQueryData(queryKeys.posts.detail(postId), (old: Post) => ({
|
|
264
|
+
...old,
|
|
265
|
+
liked: true,
|
|
266
|
+
likesCount: old.likesCount + 1,
|
|
267
|
+
}));
|
|
268
|
+
return { previous };
|
|
269
|
+
},
|
|
270
|
+
onError: (err, postId, context) => {
|
|
271
|
+
queryClient.setQueryData(queryKeys.posts.detail(postId), context?.previous);
|
|
272
|
+
},
|
|
273
|
+
onSettled: (data, error, postId) => {
|
|
274
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.posts.detail(postId) });
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Prefetching (For Better UX)
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
<Link
|
|
283
|
+
href={`/users/${user.id}`}
|
|
284
|
+
onMouseEnter={() => {
|
|
285
|
+
queryClient.prefetchQuery({
|
|
286
|
+
queryKey: queryKeys.users.detail(user.id),
|
|
287
|
+
queryFn: () => userService.get(user.id),
|
|
288
|
+
});
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
{user.name}
|
|
292
|
+
</Link>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Common Mistakes
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
// ❌ Mixing server state with local state
|
|
301
|
+
const [users, setUsers] = useState([]); // DELETE THIS
|
|
302
|
+
const { data } = useQuery({...}); // USE THIS ONLY
|
|
303
|
+
|
|
304
|
+
// ❌ Fetching in useEffect
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
fetchUsers().then(setUsers); // WRONG
|
|
307
|
+
}, []);
|
|
308
|
+
// ✅ Use useQuery instead
|
|
309
|
+
|
|
310
|
+
// ❌ Missing error handling in mutation
|
|
311
|
+
const mutation = useMutation({
|
|
312
|
+
mutationFn: userService.create,
|
|
313
|
+
onSuccess: () => message.success("Created"),
|
|
314
|
+
// MISSING: onError for form validation!
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// ✅ Always handle errors
|
|
318
|
+
const mutation = useMutation({
|
|
319
|
+
mutationFn: userService.create,
|
|
320
|
+
onSuccess: () => message.success(t("created")),
|
|
321
|
+
onError: (error) => form.setFields(getFormErrors(error)),
|
|
322
|
+
});
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## When NOT to Use TanStack Query
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// ❌ For client-only state (use useState or Zustand)
|
|
331
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
332
|
+
const [selectedItems, setSelectedItems] = useState<number[]>([]);
|
|
333
|
+
|
|
334
|
+
// ❌ For derived/computed values (use useMemo)
|
|
335
|
+
const filteredUsers = useMemo(
|
|
336
|
+
() => users.filter(u => u.active),
|
|
337
|
+
[users]
|
|
338
|
+
);
|
|
339
|
+
```
|