@famgia/omnify-laravel 0.0.119 → 0.0.121

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,671 @@
1
+ # TypeScript Types Guide
2
+
3
+ > This guide defines where and how to define types in this project.
4
+
5
+ ## Type Categories
6
+
7
+ | Category | Location | Generated | Example |
8
+ | ------------------- | -------------------- | --------- | ----------------------------- |
9
+ | **Model** | `@/types/model` | ✅ Omnify | `User`, `Post` |
10
+ | **Create/Update** | `@/types/model` | ✅ Omnify | `UserCreate`, `UserUpdate` |
11
+ | **Common** | `@/types/model` | ✅ Omnify | `DateTimeString`, `LocaleMap` |
12
+ | **Validation** | `@/types/model` | ✅ Omnify | `getUserRules(locale)` |
13
+ | **Enum** | `@/types/model/enum` | ✅ Omnify | `PostStatus`, `UserRole` |
14
+ | **API Params** | Service file | ❌ Manual | `UserListParams` |
15
+ | **API Response** | `@/lib/api.ts` | ❌ Manual | `PaginatedResponse<T>` |
16
+ | **Component Props** | Component file | ❌ Manual | `UserTableProps` |
17
+
18
+ ---
19
+
20
+ ## 1. Model Types (Omnify)
21
+
22
+ **Location**: `src/types/model/`
23
+
24
+ **Source**: Auto-generated from `.omnify/schemas/`
25
+
26
+ ```typescript
27
+ // ✅ Import from @/types/model
28
+ import type { User, UserCreate, UserUpdate } from "@/types/model";
29
+ import type { DateTimeString } from "@/types/model";
30
+ import { getUserRules } from "@/types/model";
31
+
32
+ // ❌ DON'T define model types manually
33
+ interface User { ... } // WRONG - already generated
34
+ ```
35
+
36
+ ### Structure
37
+
38
+ ```
39
+ src/types/model/
40
+ ├── common.ts ❌ DO NOT EDIT
41
+ │ # LocaleMap, ValidationRule, DateTimeString
42
+ ├── base/ ❌ DO NOT EDIT
43
+ │ └── User.ts # User + UserCreate + UserUpdate
44
+ ├── rules/ ❌ DO NOT EDIT
45
+ │ └── User.rules.ts # getUserRules(), getUserDisplayName()
46
+ ├── enum/ ❌ DO NOT EDIT (if exists)
47
+ │ └── PostStatus.ts
48
+ ├── index.ts ❌ DO NOT EDIT (re-exports)
49
+ └── User.ts ✅ CAN EDIT (extension)
50
+ ```
51
+
52
+ ### Generated Types Per Model
53
+
54
+ ```typescript
55
+ // Auto-generated in base/User.ts:
56
+ interface User {
57
+ id: number;
58
+ name: string;
59
+ email: string;
60
+ created_at?: DateTimeString; // Uses DateTimeString
61
+ updated_at?: DateTimeString;
62
+ }
63
+
64
+ type UserCreate = Omit<User, 'id' | 'created_at' | 'updated_at'>;
65
+ type UserUpdate = Partial<UserCreate>;
66
+ ```
67
+
68
+ ### Extending Model Types
69
+
70
+ ```typescript
71
+ // src/types/model/User.ts (safe to edit)
72
+ import type { User as UserBase } from "./base/User";
73
+
74
+ export interface User extends UserBase {
75
+ // Frontend-only computed properties
76
+ fullName?: string;
77
+
78
+ // UI state
79
+ isSelected?: boolean;
80
+ }
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 2. Enum Types (CRITICAL)
86
+
87
+ **Location**: `@/omnify/enum/` or `@/types/model/enum/`
88
+
89
+ **ALWAYS use generated Enums - NEVER inline union types!**
90
+
91
+ ### Generated Enum Structure
92
+
93
+ ```typescript
94
+ // @/omnify/enum/ApprovalStatus.ts (auto-generated)
95
+
96
+ // 1. Enum type
97
+ export enum ApprovalStatus {
98
+ Pending = "pending",
99
+ Approved = "approved",
100
+ Rejected = "rejected",
101
+ Cancelled = "cancelled",
102
+ }
103
+
104
+ // 2. Values array (for iteration)
105
+ export const ApprovalStatusValues = [
106
+ ApprovalStatus.Pending,
107
+ ApprovalStatus.Approved,
108
+ ApprovalStatus.Rejected,
109
+ ApprovalStatus.Cancelled,
110
+ ] as const;
111
+
112
+ // 3. Type guard
113
+ export function isApprovalStatus(value: unknown): value is ApprovalStatus {
114
+ return ApprovalStatusValues.includes(value as ApprovalStatus);
115
+ }
116
+
117
+ // 4. i18n label getter
118
+ export function getApprovalStatusLabel(value: ApprovalStatus, locale: string): string {
119
+ // Returns localized label
120
+ }
121
+ ```
122
+
123
+ ### ❌ FORBIDDEN Patterns
124
+
125
+ ```typescript
126
+ // ❌ Complex type extraction - FORBIDDEN!
127
+ const status = value as NonNullable<ListParams["filter"]>["status"];
128
+
129
+ // ❌ Hardcoded union type - NOT DRY!
130
+ const status: "pending" | "approved" | "rejected" = "pending";
131
+
132
+ // ❌ String type - NO TYPE SAFETY!
133
+ const [status, setStatus] = useState<string>("");
134
+
135
+ // ❌ Hardcoded string comparisons
136
+ if (status === "pending") { ... }
137
+ ```
138
+
139
+ ### ✅ REQUIRED Patterns
140
+
141
+ ```typescript
142
+ // ✅ Import from generated enum file
143
+ import {
144
+ ApprovalStatus,
145
+ ApprovalStatusValues,
146
+ isApprovalStatus,
147
+ getApprovalStatusLabel
148
+ } from "@/omnify/enum/ApprovalStatus";
149
+
150
+ // ✅ Type assertions
151
+ const status = unknownValue as ApprovalStatus;
152
+
153
+ // ✅ State with Enum
154
+ const [status, setStatus] = useState<ApprovalStatus | "">("");
155
+ const [status, setStatus] = useState<ApprovalStatus>(ApprovalStatus.Pending);
156
+
157
+ // ✅ Enum comparisons
158
+ if (status === ApprovalStatus.Pending) { ... }
159
+
160
+ // ✅ Iterate with Values array
161
+ const options = ApprovalStatusValues.map(value => ({
162
+ value,
163
+ label: getApprovalStatusLabel(value, locale)
164
+ }));
165
+
166
+ // ✅ Type guard
167
+ if (isApprovalStatus(value)) {
168
+ // value is ApprovalStatus
169
+ }
170
+ ```
171
+
172
+ ### Select/Filter Options Pattern
173
+
174
+ ```typescript
175
+ import {
176
+ ApprovalStatus,
177
+ ApprovalStatusValues,
178
+ getApprovalStatusLabel
179
+ } from "@/omnify/enum/ApprovalStatus";
180
+ import { useLocale } from "next-intl";
181
+
182
+ function StatusFilter() {
183
+ const locale = useLocale();
184
+
185
+ // ✅ Build options from generated enum
186
+ const statusOptions = ApprovalStatusValues.map(value => ({
187
+ value,
188
+ label: getApprovalStatusLabel(value, locale)
189
+ }));
190
+
191
+ // ✅ State with Enum type
192
+ const [status, setStatus] = useState<ApprovalStatus | "all">("all");
193
+
194
+ return (
195
+ <Select
196
+ value={status}
197
+ onChange={setStatus}
198
+ options={[
199
+ { value: "all", label: t("common.all") },
200
+ ...statusOptions
201
+ ]}
202
+ />
203
+ );
204
+ }
205
+ ```
206
+
207
+ ### Filter Params with Enum
208
+
209
+ ```typescript
210
+ import { ApprovalStatus } from "@/omnify/enum/ApprovalStatus";
211
+
212
+ interface AttendanceListParams {
213
+ filter?: {
214
+ approval_status?: ApprovalStatus; // ✅ Use Enum type
215
+ };
216
+ }
217
+
218
+ // ✅ Type assertion with Enum
219
+ const handleStatusChange = (value: string) => {
220
+ setFilters(prev => ({
221
+ ...prev,
222
+ filter: {
223
+ ...prev.filter,
224
+ approval_status: value as ApprovalStatus // ✅ Not inline union type!
225
+ }
226
+ }));
227
+ };
228
+ ```
229
+
230
+ ---
231
+
232
+ ## 3. Using Generated Types
233
+
234
+ ### Create/Update Types
235
+
236
+ ```typescript
237
+ // ✅ Use Omnify-generated types
238
+ import type { User, UserCreate, UserUpdate } from "@/types/model";
239
+
240
+ const userService = {
241
+ create: (input: UserCreate) => api.post("/api/users", input),
242
+ update: (id: number, input: UserUpdate) => api.put(`/api/users/${id}`, input),
243
+ };
244
+ ```
245
+
246
+ ### Validation Rules with Ant Design Form.Item
247
+
248
+ ```typescript
249
+ import { userSchemas, getCustomerFieldLabel } from "@/types/model/User";
250
+ import { zodRule } from "@/lib/form-validation";
251
+ import { useLocale } from "next-intl";
252
+
253
+ function UserForm() {
254
+ const locale = useLocale();
255
+ const label = (key: string) => getCustomerFieldLabel(key, locale);
256
+
257
+ return (
258
+ <Form>
259
+ {/* Name */}
260
+ <Form.Item
261
+ name="name"
262
+ label={label("name")}
263
+ rules={[zodRule(userSchemas.name, label("name"))]}
264
+ >
265
+ <Input />
266
+ </Form.Item>
267
+
268
+ {/* Email */}
269
+ <Form.Item
270
+ name="email"
271
+ label={label("email")}
272
+ rules={[zodRule(userSchemas.email, label("email"))]}
273
+ >
274
+ <Input />
275
+ </Form.Item>
276
+ </Form>
277
+ );
278
+ }
279
+ ```
280
+
281
+ **Key Points:**
282
+ - Import `{model}Schemas` for Zod validation schemas
283
+ - Import `zodRule` from `@/lib/form-validation`
284
+ - Use `zodRule(schema, displayName)` in Form.Item rules
285
+ - Comment `{/* Field Name */}` before each Form.Item for clarity
286
+
287
+ ### DateTimeString
288
+
289
+ ```typescript
290
+ import type { DateTimeString } from "@/types/model";
291
+ import { formatDateTime } from "@/lib/dayjs";
292
+
293
+ interface Event {
294
+ scheduled_at: DateTimeString; // ISO 8601 UTC string
295
+ }
296
+
297
+ // Display
298
+ formatDateTime(event.scheduled_at); // "2024/01/15 19:30"
299
+ ```
300
+
301
+ ---
302
+
303
+ ## 3. API Params Types (Manual)
304
+
305
+ **Location**: Service file (colocated)
306
+
307
+ **Only define query params (not in Omnify):**
308
+
309
+ ```typescript
310
+ // services/users.ts
311
+ import type { User, UserCreate, UserUpdate } from "@/types/model";
312
+
313
+ /** Query params for listing users (GET /api/users) */
314
+ export interface UserListParams {
315
+ search?: string;
316
+ role?: string;
317
+ page?: number;
318
+ per_page?: number;
319
+ sort_by?: keyof User;
320
+ sort_order?: "asc" | "desc";
321
+ }
322
+
323
+ export const userService = {
324
+ list: (params?: UserListParams) => ...,
325
+ create: (input: UserCreate) => ..., // ← Use Omnify type
326
+ update: (id: number, input: UserUpdate) => ..., // ← Use Omnify type
327
+ };
328
+ ```
329
+
330
+ ---
331
+
332
+ ## 4. API Response Types
333
+
334
+ **Location**: `src/lib/api.ts`
335
+
336
+ **Naming**: `{Name}Response`, `Paginated{Name}`
337
+
338
+ ```typescript
339
+ // lib/api.ts
340
+
341
+ /** Laravel paginated response */
342
+ export interface PaginatedResponse<T> {
343
+ data: T[];
344
+ links: {
345
+ first: string | null;
346
+ last: string | null;
347
+ prev: string | null;
348
+ next: string | null;
349
+ };
350
+ meta: {
351
+ current_page: number;
352
+ from: number | null;
353
+ last_page: number;
354
+ per_page: number;
355
+ to: number | null;
356
+ total: number;
357
+ };
358
+ }
359
+
360
+ /** Laravel single resource response */
361
+ export interface ResourceResponse<T> {
362
+ data: T;
363
+ }
364
+
365
+ /** Laravel validation error (422) */
366
+ export interface ValidationError {
367
+ message: string;
368
+ errors: Record<string, string[]>;
369
+ }
370
+ ```
371
+
372
+ ### Usage in Service
373
+
374
+ ```typescript
375
+ import api, { PaginatedResponse } from "@/lib/api";
376
+ import type { User } from "@/types/model";
377
+
378
+ export const userService = {
379
+ list: async (params?: UserListParams): Promise<PaginatedResponse<User>> => {
380
+ const { data } = await api.get("/api/users", { params });
381
+ return data;
382
+ },
383
+ };
384
+ ```
385
+
386
+ ---
387
+
388
+ ## 4. Component Props Types
389
+
390
+ **Location**: Same file as component (inline)
391
+
392
+ **Naming**: `{Component}Props`
393
+
394
+ ```typescript
395
+ // components/tables/UserTable.tsx
396
+
397
+ import type { User } from "@/types/model";
398
+ import type { PaginatedResponse } from "@/lib/api";
399
+
400
+ // ─────────────────────────────────────────────────────────────────
401
+ // Props - Define at top of file
402
+ // ─────────────────────────────────────────────────────────────────
403
+
404
+ interface UserTableProps {
405
+ users: User[];
406
+ loading?: boolean;
407
+ pagination?: PaginatedResponse<User>["meta"];
408
+ onPageChange?: (page: number) => void;
409
+ onEdit?: (user: User) => void;
410
+ onDelete?: (user: User) => void;
411
+ }
412
+
413
+ // ─────────────────────────────────────────────────────────────────
414
+ // Component
415
+ // ─────────────────────────────────────────────────────────────────
416
+
417
+ export function UserTable({
418
+ users,
419
+ loading = false,
420
+ pagination,
421
+ onPageChange,
422
+ onEdit,
423
+ onDelete,
424
+ }: UserTableProps) {
425
+ return <Table ... />;
426
+ }
427
+ ```
428
+
429
+ ### When to Export Props
430
+
431
+ ```typescript
432
+ // ✅ Export if other components need it
433
+ export interface UserTableProps { ... }
434
+
435
+ // ✅ Don't export if only used internally
436
+ interface UserTableProps { ... }
437
+ ```
438
+
439
+ ---
440
+
441
+ ## 5. Hook Types
442
+
443
+ **Location**: Hook file (inline or inferred)
444
+
445
+ **Approach**: Let TypeScript infer return types when possible
446
+
447
+ ```typescript
448
+ // hooks/useUsers.ts
449
+
450
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
451
+ import { userService, UserCreateInput } from "@/services/users";
452
+ import { queryKeys } from "@/lib/queryKeys";
453
+
454
+ export function useUsers(params?: UserListParams) {
455
+ // Return type is inferred from userService.list
456
+ return useQuery({
457
+ queryKey: queryKeys.users.list(params),
458
+ queryFn: () => userService.list(params),
459
+ });
460
+ }
461
+
462
+ export function useCreateUser() {
463
+ const queryClient = useQueryClient();
464
+
465
+ // Return type is inferred from useMutation
466
+ return useMutation({
467
+ mutationFn: (input: UserCreateInput) => userService.create(input),
468
+ onSuccess: () => {
469
+ queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
470
+ },
471
+ });
472
+ }
473
+ ```
474
+
475
+ ### When to Define Return Type
476
+
477
+ ```typescript
478
+ // ✅ Let TypeScript infer (simpler, less maintenance)
479
+ export function useUsers(params?: UserListParams) {
480
+ return useQuery({ ... });
481
+ }
482
+
483
+ // ✅ Define explicitly if complex or for documentation
484
+ export function useAuth(): {
485
+ user: User | undefined;
486
+ isLoading: boolean;
487
+ login: (input: LoginInput) => Promise<void>;
488
+ logout: () => Promise<void>;
489
+ } {
490
+ ...
491
+ }
492
+ ```
493
+
494
+ ---
495
+
496
+ ## 6. Shared/Utility Types
497
+
498
+ **Location**: `src/types/index.ts` (only if used across many files)
499
+
500
+ ```typescript
501
+ // types/index.ts
502
+
503
+ /** Common ID type */
504
+ export type ID = number;
505
+
506
+ /** Nullable type helper */
507
+ export type Nullable<T> = T | null;
508
+
509
+ /** Make specific keys optional */
510
+ export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
511
+
512
+ /** Extract array element type */
513
+ export type ArrayElement<T> = T extends (infer U)[] ? U : never;
514
+ ```
515
+
516
+ ### When to Use Shared Types
517
+
518
+ ```typescript
519
+ // ✅ Use shared types for truly common patterns
520
+ import type { ID, Nullable } from "@/types";
521
+
522
+ interface Post {
523
+ id: ID;
524
+ author_id: ID;
525
+ published_at: Nullable<string>;
526
+ }
527
+
528
+ // ❌ Don't over-abstract
529
+ // Bad: Creating shared types for every little thing
530
+ export type UserName = string; // Just use string
531
+ export type UserId = number; // Just use number
532
+ ```
533
+
534
+ ---
535
+
536
+ ## Type Definition Checklist
537
+
538
+ ### Before Creating a Type
539
+
540
+ 1. **Is it a Model?** → Use `@/types/model` (Omnify)
541
+ 2. **Is it API input?** → Define in service file
542
+ 3. **Is it API response?** → Use/extend types in `lib/api.ts`
543
+ 4. **Is it component props?** → Define in component file
544
+ 5. **Is it used in 3+ places?** → Consider `types/index.ts`
545
+
546
+ ### Type Naming Conventions
547
+
548
+ | Type | Pattern | Example |
549
+ | ------------ | -------------------- | ------------------- |
550
+ | Model | PascalCase | `User`, `Post` |
551
+ | Create Input | `{Model}CreateInput` | `UserCreateInput` |
552
+ | Update Input | `{Model}UpdateInput` | `UserUpdateInput` |
553
+ | List Params | `{Model}ListParams` | `UserListParams` |
554
+ | Props | `{Component}Props` | `UserTableProps` |
555
+ | Response | `{Name}Response` | `PaginatedResponse` |
556
+
557
+ ---
558
+
559
+ ## Complete Example
560
+
561
+ ```typescript
562
+ // ═══════════════════════════════════════════════════════════════════
563
+ // types/model/User.ts (Omnify extension)
564
+ // ═══════════════════════════════════════════════════════════════════
565
+ import type { User as UserBase } from "./base/User";
566
+
567
+ export interface User extends UserBase {
568
+ // Add frontend-only properties if needed
569
+ }
570
+
571
+ // ═══════════════════════════════════════════════════════════════════
572
+ // services/users.ts
573
+ // ═══════════════════════════════════════════════════════════════════
574
+ import api, { PaginatedResponse } from "@/lib/api";
575
+ import type { User } from "@/types/model";
576
+
577
+ export interface UserCreateInput {
578
+ name: string;
579
+ email: string;
580
+ password: string;
581
+ }
582
+
583
+ export interface UserUpdateInput {
584
+ name?: string;
585
+ email?: string;
586
+ }
587
+
588
+ export interface UserListParams {
589
+ search?: string;
590
+ page?: number;
591
+ }
592
+
593
+ export const userService = {
594
+ list: async (params?: UserListParams): Promise<PaginatedResponse<User>> => {
595
+ const { data } = await api.get("/api/users", { params });
596
+ return data;
597
+ },
598
+ get: async (id: number): Promise<User> => {
599
+ const { data } = await api.get(`/api/users/${id}`);
600
+ return data.data ?? data;
601
+ },
602
+ create: async (input: UserCreateInput): Promise<User> => {
603
+ const { data } = await api.post("/api/users", input);
604
+ return data.data ?? data;
605
+ },
606
+ update: async (id: number, input: UserUpdateInput): Promise<User> => {
607
+ const { data } = await api.put(`/api/users/${id}`, input);
608
+ return data.data ?? data;
609
+ },
610
+ delete: async (id: number): Promise<void> => {
611
+ await api.delete(`/api/users/${id}`);
612
+ },
613
+ };
614
+
615
+ // ═══════════════════════════════════════════════════════════════════
616
+ // components/tables/UserTable.tsx
617
+ // ═══════════════════════════════════════════════════════════════════
618
+ import type { User } from "@/types/model";
619
+
620
+ interface UserTableProps {
621
+ users: User[];
622
+ loading?: boolean;
623
+ onEdit?: (user: User) => void;
624
+ }
625
+
626
+ export function UserTable({ users, loading, onEdit }: UserTableProps) {
627
+ return <Table dataSource={users} loading={loading} ... />;
628
+ }
629
+
630
+ // ═══════════════════════════════════════════════════════════════════
631
+ // app/(dashboard)/users/page.tsx
632
+ // ═══════════════════════════════════════════════════════════════════
633
+ "use client";
634
+
635
+ import { useQuery } from "@tanstack/react-query";
636
+ import { userService, UserListParams } from "@/services/users";
637
+ import { UserTable } from "@/components/tables/UserTable";
638
+ import { queryKeys } from "@/lib/queryKeys";
639
+
640
+ export default function UsersPage() {
641
+ const [params, setParams] = useState<UserListParams>({ page: 1 });
642
+
643
+ const { data, isLoading } = useQuery({
644
+ queryKey: queryKeys.users.list(params),
645
+ queryFn: () => userService.list(params),
646
+ });
647
+
648
+ return (
649
+ <UserTable
650
+ users={data?.data ?? []}
651
+ loading={isLoading}
652
+ onEdit={(user) => router.push(`/users/${user.id}/edit`)}
653
+ />
654
+ );
655
+ }
656
+ ```
657
+
658
+ ---
659
+
660
+ ## Summary
661
+
662
+ | Type | Location | Why |
663
+ | -------- | -------------------- | ------------------------- |
664
+ | Model | `@/types/model` | Synced with DB via Omnify |
665
+ | Input | Service file | Colocated with API logic |
666
+ | Response | `lib/api.ts` | Shared Laravel patterns |
667
+ | Props | Component file | Colocated with component |
668
+ | Hook | Hook file (inferred) | TypeScript handles it |
669
+ | Utility | `types/index.ts` | Only if widely used |
670
+
671
+ **Philosophy**: Keep types close to their usage. Don't over-organize.