@famgia/omnify-laravel 0.0.119 → 0.0.120
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/dist/{chunk-7I6UNXOD.js → chunk-NMX3TLZT.js} +8 -1
- package/dist/{chunk-7I6UNXOD.js.map → chunk-NMX3TLZT.js.map} +1 -1
- package/dist/index.cjs +7 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.cjs +7 -0
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.js +1 -1
- package/package.json +4 -4
- package/stubs/ai-guides/claude-checklists/react.md.stub +108 -0
- package/stubs/ai-guides/cursor/omnify-schema.mdc.stub +339 -0
- package/stubs/ai-guides/cursor/react-design.mdc.stub +693 -0
- package/stubs/ai-guides/cursor/react-form.mdc.stub +277 -0
- package/stubs/ai-guides/cursor/react-services.mdc.stub +304 -0
- package/stubs/ai-guides/cursor/react.mdc.stub +336 -0
- package/stubs/ai-guides/cursor/schema-create.mdc.stub +344 -0
- package/stubs/ai-guides/react/README.md.stub +221 -0
- package/stubs/ai-guides/react/antd-guide.md.stub +457 -0
- package/stubs/ai-guides/react/checklist.md.stub +108 -0
- package/stubs/ai-guides/react/datetime-guide.md.stub +137 -0
- package/stubs/ai-guides/react/design-philosophy.md.stub +363 -0
- package/stubs/ai-guides/react/i18n-guide.md.stub +211 -0
- package/stubs/ai-guides/react/laravel-integration.md.stub +181 -0
- package/stubs/ai-guides/react/service-pattern.md.stub +180 -0
- package/stubs/ai-guides/react/tanstack-query.md.stub +339 -0
- package/stubs/ai-guides/react/types-guide.md.stub +671 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "React form development with Ant Design + Zod validation. Covers form instances, zodRule helper, Japanese compound fields (JapaneseName, JapaneseAddress), and backend 422 error handling. Apply when creating forms."
|
|
3
|
+
globs: ["{{TYPESCRIPT_BASE}}/**/*Form*.tsx", "{{TYPESCRIPT_BASE}}/**/*form*.tsx"]
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Form Development Guide
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { Form, Input, Button, Space, Card, Divider } from 'antd';
|
|
13
|
+
import type { FormInstance } from 'antd';
|
|
14
|
+
import { zodRule, setZodLocale } from '@/omnify/lib';
|
|
15
|
+
import { OmnifyForm } from '@/omnify/components';
|
|
16
|
+
import {
|
|
17
|
+
type Customer,
|
|
18
|
+
type CustomerCreate,
|
|
19
|
+
customerSchemas,
|
|
20
|
+
customerI18n,
|
|
21
|
+
getCustomerFieldLabel,
|
|
22
|
+
getCustomerFieldPlaceholder,
|
|
23
|
+
} from '@/omnify/schemas';
|
|
24
|
+
|
|
25
|
+
const LOCALE = 'ja'; // TODO: Get from context
|
|
26
|
+
|
|
27
|
+
export function CustomerForm({
|
|
28
|
+
form,
|
|
29
|
+
initialValues,
|
|
30
|
+
onSubmit,
|
|
31
|
+
loading = false,
|
|
32
|
+
isEdit = false,
|
|
33
|
+
onCancel,
|
|
34
|
+
}: {
|
|
35
|
+
form: FormInstance<CustomerCreate>;
|
|
36
|
+
initialValues?: Partial<Customer>;
|
|
37
|
+
onSubmit: (values: CustomerCreate) => void;
|
|
38
|
+
loading?: boolean;
|
|
39
|
+
isEdit?: boolean;
|
|
40
|
+
onCancel?: () => void;
|
|
41
|
+
}) {
|
|
42
|
+
// Set locale for validation messages
|
|
43
|
+
setZodLocale(LOCALE);
|
|
44
|
+
|
|
45
|
+
// Helper functions
|
|
46
|
+
const label = (key: string) => getCustomerFieldLabel(key, LOCALE);
|
|
47
|
+
const placeholder = (key: string) => getCustomerFieldPlaceholder(key, LOCALE);
|
|
48
|
+
const rule = (key: keyof typeof customerSchemas) =>
|
|
49
|
+
zodRule(customerSchemas[key], label(key));
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Card>
|
|
53
|
+
<Form
|
|
54
|
+
form={form}
|
|
55
|
+
layout="horizontal"
|
|
56
|
+
labelCol={{ span: 6 }}
|
|
57
|
+
wrapperCol={{ span: 18 }}
|
|
58
|
+
initialValues={initialValues}
|
|
59
|
+
onFinish={onSubmit}
|
|
60
|
+
style={{ maxWidth: 800 }}
|
|
61
|
+
>
|
|
62
|
+
{/* Section: Contact */}
|
|
63
|
+
<Divider orientation="left">Contact</Divider>
|
|
64
|
+
|
|
65
|
+
<Form.Item
|
|
66
|
+
name="email"
|
|
67
|
+
label={label('email')}
|
|
68
|
+
rules={[rule('email')]}
|
|
69
|
+
>
|
|
70
|
+
<Input type="email" placeholder={placeholder('email')} />
|
|
71
|
+
</Form.Item>
|
|
72
|
+
|
|
73
|
+
{/* Submit Buttons */}
|
|
74
|
+
<Form.Item wrapperCol={{ offset: 6, span: 18 }}>
|
|
75
|
+
<Space>
|
|
76
|
+
<Button type="primary" htmlType="submit" loading={loading}>
|
|
77
|
+
{isEdit ? 'Update' : 'Create'}
|
|
78
|
+
</Button>
|
|
79
|
+
{onCancel && <Button onClick={onCancel}>Cancel</Button>}
|
|
80
|
+
</Space>
|
|
81
|
+
</Form.Item>
|
|
82
|
+
</Form>
|
|
83
|
+
</Card>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Key Patterns
|
|
89
|
+
|
|
90
|
+
### 1. Imports
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// Omnify utilities
|
|
94
|
+
import { zodRule, setZodLocale } from '@/omnify/lib';
|
|
95
|
+
import { OmnifyForm } from '@/omnify/components';
|
|
96
|
+
|
|
97
|
+
// Model-specific (generated by Omnify)
|
|
98
|
+
import {
|
|
99
|
+
type Customer,
|
|
100
|
+
type CustomerCreate,
|
|
101
|
+
customerSchemas,
|
|
102
|
+
customerI18n,
|
|
103
|
+
getCustomerFieldLabel,
|
|
104
|
+
getCustomerFieldPlaceholder,
|
|
105
|
+
} from '@/omnify/schemas';
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 2. Helper Functions (MUST define in every form)
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
const LOCALE = 'ja'; // Get from context in real app
|
|
112
|
+
|
|
113
|
+
// Set locale once at start
|
|
114
|
+
setZodLocale(LOCALE);
|
|
115
|
+
|
|
116
|
+
// Define helper functions
|
|
117
|
+
const label = (key: string) => getCustomerFieldLabel(key, LOCALE);
|
|
118
|
+
const placeholder = (key: string) => getCustomerFieldPlaceholder(key, LOCALE);
|
|
119
|
+
const rule = (key: keyof typeof customerSchemas) =>
|
|
120
|
+
zodRule(customerSchemas[key], label(key));
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 3. Form.Item Pattern
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
<Form.Item
|
|
127
|
+
name="email" // Field name (matches backend)
|
|
128
|
+
label={label('email')} // i18n label
|
|
129
|
+
rules={[rule('email')]} // Zod validation
|
|
130
|
+
>
|
|
131
|
+
<Input
|
|
132
|
+
type="email"
|
|
133
|
+
placeholder={placeholder('email')} // i18n placeholder
|
|
134
|
+
/>
|
|
135
|
+
</Form.Item>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 4. Section Dividers
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
<Divider orientation="left">Contact Info</Divider>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Compound Fields (Japan Plugin)
|
|
145
|
+
|
|
146
|
+
For Japanese compound types, use `OmnifyForm` components:
|
|
147
|
+
|
|
148
|
+
### JapaneseName
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
<OmnifyForm.JapaneseName
|
|
152
|
+
schemas={customerSchemas}
|
|
153
|
+
i18n={customerI18n}
|
|
154
|
+
prefix="name"
|
|
155
|
+
required
|
|
156
|
+
/>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### JapaneseAddress
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
<OmnifyForm.JapaneseAddress
|
|
163
|
+
form={form}
|
|
164
|
+
schemas={customerSchemas}
|
|
165
|
+
i18n={customerI18n}
|
|
166
|
+
prefix="address"
|
|
167
|
+
/>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### JapaneseBank
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
<OmnifyForm.JapaneseBank
|
|
174
|
+
schemas={customerSchemas}
|
|
175
|
+
i18n={customerI18n}
|
|
176
|
+
prefix="bank"
|
|
177
|
+
/>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Props Pattern
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
interface CustomerFormProps {
|
|
184
|
+
form: FormInstance<CustomerCreate>; // REQUIRED - from parent
|
|
185
|
+
initialValues?: Partial<Customer>; // For edit mode
|
|
186
|
+
onSubmit: (values: CustomerCreate) => void;
|
|
187
|
+
loading?: boolean;
|
|
188
|
+
isEdit?: boolean;
|
|
189
|
+
onCancel?: () => void;
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Page Component (Parent)
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
import { Form } from 'antd';
|
|
197
|
+
import { useFormMutation } from '@/hooks/useFormMutation';
|
|
198
|
+
import { customerService } from '@/services/customers';
|
|
199
|
+
import { queryKeys } from '@/lib/queryKeys';
|
|
200
|
+
import { CustomerForm } from '@/features/customers/CustomerForm';
|
|
201
|
+
import type { CustomerCreate } from '@/omnify/schemas';
|
|
202
|
+
|
|
203
|
+
export default function CreateCustomerPage() {
|
|
204
|
+
const [form] = Form.useForm<CustomerCreate>();
|
|
205
|
+
|
|
206
|
+
const { mutate, isPending } = useFormMutation({
|
|
207
|
+
form,
|
|
208
|
+
mutationFn: customerService.create,
|
|
209
|
+
invalidateKeys: [queryKeys.customers.all],
|
|
210
|
+
successMessage: "messages.created",
|
|
211
|
+
redirectTo: "/customers",
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<CustomerForm
|
|
216
|
+
form={form}
|
|
217
|
+
onSubmit={(values) => mutate(values)}
|
|
218
|
+
loading={isPending}
|
|
219
|
+
/>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Backend Errors (422)
|
|
225
|
+
|
|
226
|
+
`useFormMutation` automatically handles:
|
|
227
|
+
1. `form.setFields(getFormErrors(error))` - display on form fields
|
|
228
|
+
2. `message.error(validationMessage)` - toast message
|
|
229
|
+
|
|
230
|
+
**Field names MUST match Laravel:**
|
|
231
|
+
```tsx
|
|
232
|
+
// Laravel: { errors: { "name_lastname": ["..."] } }
|
|
233
|
+
<Form.Item name="name_lastname"> // ✅ Matches
|
|
234
|
+
<Form.Item name="lastName"> // ❌ Doesn't match
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Checklist
|
|
238
|
+
|
|
239
|
+
- [ ] Import from `@/omnify/lib`, `@/omnify/components`, `@/omnify/schemas`
|
|
240
|
+
- [ ] Define `label()`, `placeholder()`, `rule()` helper functions
|
|
241
|
+
- [ ] Call `setZodLocale(LOCALE)` at start
|
|
242
|
+
- [ ] Form receives `form` prop from parent (not create own)
|
|
243
|
+
- [ ] Use `Divider` for sections
|
|
244
|
+
- [ ] Use `OmnifyForm.*` for compound fields (JapaneseName, etc.)
|
|
245
|
+
- [ ] Field names match Laravel field names
|
|
246
|
+
- [ ] Submit buttons with loading state
|
|
247
|
+
|
|
248
|
+
## Common Mistakes
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
// ❌ Wrong - Create form instance in form component
|
|
252
|
+
function MyForm() {
|
|
253
|
+
const [form] = Form.useForm(); // DON'T create here
|
|
254
|
+
return <Form form={form}>...
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ✅ Correct - Receive from parent
|
|
258
|
+
function MyForm({ form }: { form: FormInstance }) {
|
|
259
|
+
return <Form form={form}>...
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
// ❌ Wrong - Inline validation message
|
|
265
|
+
rules={[{ required: true, message: 'Email is required' }]}
|
|
266
|
+
|
|
267
|
+
// ✅ Correct - Use zodRule with i18n
|
|
268
|
+
rules={[rule('email')]}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
// ❌ Wrong - Hardcoded placeholder
|
|
273
|
+
<Input placeholder="Enter email" />
|
|
274
|
+
|
|
275
|
+
// ✅ Correct - Use placeholder helper
|
|
276
|
+
<Input placeholder={placeholder('email')} />
|
|
277
|
+
```
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "React service layer pattern: MUST read api-docs.json first, use TanStack Query for server state, define types from OpenAPI response. Apply when creating API integration or service files."
|
|
3
|
+
globs: ["{{TYPESCRIPT_BASE}}/services/**/*.ts"]
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Frontend Services Rules
|
|
8
|
+
|
|
9
|
+
> **API Reference:** `storage/api-docs/api-docs.json` (OpenAPI 3.0)
|
|
10
|
+
> **Base Config:** `lib/api.ts`
|
|
11
|
+
|
|
12
|
+
## ⛔ MUST: Check api-docs.json FIRST!
|
|
13
|
+
|
|
14
|
+
**BEFORE writing ANY service code, MUST read `storage/api-docs/api-docs.json`:**
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# 1. ALWAYS read api-docs.json first
|
|
18
|
+
cat storage/api-docs/api-docs.json | jq '.paths'
|
|
19
|
+
|
|
20
|
+
# 2. Check specific endpoint
|
|
21
|
+
cat storage/api-docs/api-docs.json | jq '.paths["/api/users"]'
|
|
22
|
+
|
|
23
|
+
# 3. Check request/response schemas
|
|
24
|
+
cat storage/api-docs/api-docs.json | jq '.components.schemas'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Checklist (MANDATORY)
|
|
28
|
+
|
|
29
|
+
- [ ] **Read api-docs.json** - NO guessing!
|
|
30
|
+
- [ ] **Verify endpoint exists** - URL, method must match
|
|
31
|
+
- [ ] **Copy exact parameter names** - `filter[search]` not `search`
|
|
32
|
+
- [ ] **Match request body schema** - required fields, types
|
|
33
|
+
- [ ] **Match response schema** - data structure
|
|
34
|
+
|
|
35
|
+
### ❌ DON'T:
|
|
36
|
+
```typescript
|
|
37
|
+
// ❌ Guessing parameter names
|
|
38
|
+
search?: string; // WRONG - API uses filter[search]
|
|
39
|
+
sort_by?: string; // WRONG - API uses sort
|
|
40
|
+
sort_order?: "asc"|"desc"; // WRONG - API uses "-field" format
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### ✅ DO:
|
|
44
|
+
```typescript
|
|
45
|
+
// ✅ Copy exactly from api-docs.json
|
|
46
|
+
|
|
47
|
+
// Sort has enum? → Create union type!
|
|
48
|
+
export type UserSortField =
|
|
49
|
+
| "id" | "-id"
|
|
50
|
+
| "name_lastname" | "-name_lastname"
|
|
51
|
+
| "email" | "-email"
|
|
52
|
+
| "created_at" | "-created_at";
|
|
53
|
+
|
|
54
|
+
export interface UserListParams {
|
|
55
|
+
sort?: UserSortField; // Correct: type-safe from enum
|
|
56
|
+
filter?: {
|
|
57
|
+
search?: string; // Correct: maps to filter[search]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Regenerate OpenAPI docs if needed
|
|
64
|
+
php artisan l5-swagger:generate
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Service File Structure
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
/**
|
|
71
|
+
* {Resource} Service
|
|
72
|
+
*
|
|
73
|
+
* CRUD operations for {Resource} resource.
|
|
74
|
+
* Maps to Laravel {Resource}Controller.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
import api, { PaginatedResponse } from "@/lib/api";
|
|
78
|
+
import type { Resource, ResourceCreate, ResourceUpdate } from "@/types/model";
|
|
79
|
+
|
|
80
|
+
// =============================================================================
|
|
81
|
+
// Types - Only query params (Create/Update come from Omnify)
|
|
82
|
+
// =============================================================================
|
|
83
|
+
|
|
84
|
+
/** Sort fields - MUST match api-docs.json enum! */
|
|
85
|
+
export type ResourceSortField =
|
|
86
|
+
| "id" | "-id"
|
|
87
|
+
| "created_at" | "-created_at"
|
|
88
|
+
| "updated_at" | "-updated_at";
|
|
89
|
+
// Add other fields from api-docs.json enum
|
|
90
|
+
|
|
91
|
+
/** Query params for listing resources */
|
|
92
|
+
export interface ResourceListParams {
|
|
93
|
+
page?: number;
|
|
94
|
+
per_page?: number;
|
|
95
|
+
sort?: ResourceSortField; // Type-safe from enum
|
|
96
|
+
filter?: {
|
|
97
|
+
search?: string;
|
|
98
|
+
// Add other filters from api-docs.json
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// =============================================================================
|
|
103
|
+
// Service
|
|
104
|
+
// =============================================================================
|
|
105
|
+
|
|
106
|
+
const BASE_URL = "/api/resources";
|
|
107
|
+
|
|
108
|
+
export const resourceService = {
|
|
109
|
+
/**
|
|
110
|
+
* Axios auto-serializes nested objects:
|
|
111
|
+
* { filter: { search: "x" } } → ?filter[search]=x
|
|
112
|
+
*/
|
|
113
|
+
list: async (params?: ResourceListParams): Promise<PaginatedResponse<Resource>> => {
|
|
114
|
+
const { data } = await api.get(BASE_URL, { params });
|
|
115
|
+
return data;
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
get: async (id: number): Promise<Resource> => {
|
|
119
|
+
const { data } = await api.get(`${BASE_URL}/${id}`);
|
|
120
|
+
return data.data ?? data;
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
create: async (input: ResourceCreate): Promise<Resource> => {
|
|
124
|
+
const { data } = await api.post(BASE_URL, input);
|
|
125
|
+
return data.data ?? data;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
update: async (id: number, input: ResourceUpdate): Promise<Resource> => {
|
|
129
|
+
const { data } = await api.put(`${BASE_URL}/${id}`, input);
|
|
130
|
+
return data.data ?? data;
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
delete: async (id: number): Promise<void> => {
|
|
134
|
+
await api.delete(`${BASE_URL}/${id}`);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Types Rule
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// ✅ Import model types from Omnify
|
|
143
|
+
import type { User, UserCreate, UserUpdate } from "@/types/model";
|
|
144
|
+
|
|
145
|
+
// ✅ Define only query/filter params locally
|
|
146
|
+
export interface UserListParams {
|
|
147
|
+
search?: string;
|
|
148
|
+
page?: number;
|
|
149
|
+
per_page?: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ❌ DON'T redefine model types
|
|
153
|
+
export interface User { ... } // WRONG - use Omnify
|
|
154
|
+
export interface UserCreateInput { ... } // WRONG - use UserCreate
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Response Handling
|
|
158
|
+
|
|
159
|
+
### Paginated List (GET /api/resources)
|
|
160
|
+
```typescript
|
|
161
|
+
// OpenAPI: Returns { data: T[], links: {...}, meta: {...} }
|
|
162
|
+
list: async (params): Promise<PaginatedResponse<Resource>> => {
|
|
163
|
+
const { data } = await api.get(BASE_URL, { params });
|
|
164
|
+
return data; // Full response with data, links, meta
|
|
165
|
+
},
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Single Resource (GET /api/resources/:id)
|
|
169
|
+
```typescript
|
|
170
|
+
// OpenAPI: Returns { data: T }
|
|
171
|
+
get: async (id: number): Promise<Resource> => {
|
|
172
|
+
const { data } = await api.get(`${BASE_URL}/${id}`);
|
|
173
|
+
return data.data ?? data; // Unwrap { data: T } wrapper
|
|
174
|
+
},
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Create/Update (POST/PUT)
|
|
178
|
+
```typescript
|
|
179
|
+
// OpenAPI: Returns { data: T }
|
|
180
|
+
create: async (input: ResourceCreate): Promise<Resource> => {
|
|
181
|
+
const { data } = await api.post(BASE_URL, input);
|
|
182
|
+
return data.data ?? data; // Unwrap { data: T } wrapper
|
|
183
|
+
},
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Delete (DELETE /api/resources/:id)
|
|
187
|
+
```typescript
|
|
188
|
+
// OpenAPI: Returns 204 No Content
|
|
189
|
+
delete: async (id: number): Promise<void> => {
|
|
190
|
+
await api.delete(`${BASE_URL}/${id}`);
|
|
191
|
+
},
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Auth Service Pattern (Sanctum)
|
|
195
|
+
|
|
196
|
+
For auth endpoints that need CSRF:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import api, { csrf } from "@/lib/api";
|
|
200
|
+
|
|
201
|
+
export const authService = {
|
|
202
|
+
login: async (input: LoginInput): Promise<void> => {
|
|
203
|
+
await csrf(); // Required for Sanctum
|
|
204
|
+
await api.post("/login", input);
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
logout: async (): Promise<void> => {
|
|
208
|
+
await api.post("/logout");
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
me: async (): Promise<User> => {
|
|
212
|
+
const { data } = await api.get<User>("/api/user");
|
|
213
|
+
return data;
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## OpenAPI Parameter Mapping
|
|
219
|
+
|
|
220
|
+
Axios auto-serializes nested objects → **NO transform needed!**
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// Frontend interface
|
|
224
|
+
{ filter: { search: "John" }, sort: "-created_at", page: 1 }
|
|
225
|
+
|
|
226
|
+
// Axios auto-serializes to:
|
|
227
|
+
// ?filter[search]=John&sort=-created_at&page=1
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
| OpenAPI Parameter | Frontend Interface | Notes |
|
|
231
|
+
| ----------------- | ------------------ | --------------------- |
|
|
232
|
+
| `filter[search]` | `filter.search` | Axios auto-serializes |
|
|
233
|
+
| `page` | `page` | Direct |
|
|
234
|
+
| `per_page` | `per_page` | Direct |
|
|
235
|
+
| `sort` | `sort` | Use enum type! |
|
|
236
|
+
|
|
237
|
+
### Sort Field - MUST use enum!
|
|
238
|
+
```typescript
|
|
239
|
+
// ❌ WRONG - not type-safe
|
|
240
|
+
sort?: string;
|
|
241
|
+
|
|
242
|
+
// ✅ CORRECT - copy enum from api-docs.json
|
|
243
|
+
export type ResourceSortField =
|
|
244
|
+
| "id" | "-id"
|
|
245
|
+
| "created_at" | "-created_at";
|
|
246
|
+
|
|
247
|
+
sort?: ResourceSortField; // Type-safe!
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Error Handling
|
|
251
|
+
|
|
252
|
+
Errors are handled globally in `lib/api.ts`. Services should:
|
|
253
|
+
- **NOT** catch errors (let them propagate to mutations/queries)
|
|
254
|
+
- **NOT** show messages (handled by TanStack Query callbacks)
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// ❌ Wrong
|
|
258
|
+
create: async (input) => {
|
|
259
|
+
try {
|
|
260
|
+
const { data } = await api.post(BASE_URL, input);
|
|
261
|
+
return data.data;
|
|
262
|
+
} catch (e) {
|
|
263
|
+
console.error(e); // DON'T catch here
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
// ✅ Correct - let errors propagate
|
|
268
|
+
create: async (input) => {
|
|
269
|
+
const { data } = await api.post(BASE_URL, input);
|
|
270
|
+
return data.data ?? data;
|
|
271
|
+
},
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Naming Conventions
|
|
275
|
+
|
|
276
|
+
| Item | Convention | Example |
|
|
277
|
+
| -------------- | --------------------------- | ---------------------------- |
|
|
278
|
+
| File | `{resource}.ts` (singular) | `users.ts`, `auth.ts` |
|
|
279
|
+
| Service object | `{resource}Service` | `userService`, `authService` |
|
|
280
|
+
| List params | `{Resource}ListParams` | `UserListParams` |
|
|
281
|
+
| Base URL | `/api/{resources}` (plural) | `/api/users` |
|
|
282
|
+
|
|
283
|
+
## Checklist Before Creating Service
|
|
284
|
+
|
|
285
|
+
- [ ] API endpoint exists in `storage/api-docs/api-docs.json`
|
|
286
|
+
- [ ] Omnify types exist in `types/model/`
|
|
287
|
+
- [ ] Request body matches OpenAPI `requestBody.content.application/json.schema`
|
|
288
|
+
- [ ] Response matches OpenAPI `responses.200.content.application/json.schema`
|
|
289
|
+
- [ ] Parameters match OpenAPI `parameters[]`
|
|
290
|
+
|
|
291
|
+
## Quick Reference: Current API Endpoints
|
|
292
|
+
|
|
293
|
+
> From `storage/api-docs/api-docs.json`:
|
|
294
|
+
|
|
295
|
+
### Users API
|
|
296
|
+
| Method | Endpoint | Description |
|
|
297
|
+
| ------ | ----------------- | -------------------------------- |
|
|
298
|
+
| GET | `/api/users` | Paginated list with search, sort |
|
|
299
|
+
| POST | `/api/users` | Create user |
|
|
300
|
+
| GET | `/api/users/{id}` | Get single user |
|
|
301
|
+
| PUT | `/api/users/{id}` | Update user |
|
|
302
|
+
| DELETE | `/api/users/{id}` | Delete user |
|
|
303
|
+
|
|
304
|
+
**Sort fields:** `id`, `name_lastname`, `name_firstname`, `email`, `created_at`, `updated_at`
|