@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.
@@ -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`