@famgia/omnify-typescript 0.0.27 → 0.0.29
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/ai-guides/antdesign-guide.md +401 -0
- package/ai-guides/typescript-guide.md +254 -0
- package/package.json +4 -3
- package/scripts/postinstall.js +21 -226
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# Omnify + Ant Design Integration Guide
|
|
2
|
+
|
|
3
|
+
This guide shows how to use Omnify-generated validation rules with Ant Design Forms.
|
|
4
|
+
|
|
5
|
+
## Generated Files
|
|
6
|
+
|
|
7
|
+
Omnify generates validation rules in `rules/` directory:
|
|
8
|
+
- `rules/{Model}.rules.ts` - Validation rules for each model
|
|
9
|
+
|
|
10
|
+
## File Structure
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// rules/User.rules.ts
|
|
14
|
+
|
|
15
|
+
// Display name for model (multi-locale)
|
|
16
|
+
export const UserDisplayName: LocaleMap = {
|
|
17
|
+
ja: 'ユーザー',
|
|
18
|
+
en: 'User',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Display names for properties (multi-locale)
|
|
22
|
+
export const UserPropertyDisplayNames: Record<string, LocaleMap> = {
|
|
23
|
+
name: { ja: '名前', en: 'Name' },
|
|
24
|
+
email: { ja: 'メールアドレス', en: 'Email' },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Validation rules with multi-locale messages
|
|
28
|
+
export const UserRules: Record<string, ValidationRule[]> = {
|
|
29
|
+
name: [
|
|
30
|
+
{ required: true, message: { ja: '名前は必須です', en: 'Name is required' } },
|
|
31
|
+
{ max: 100, message: { ja: '名前は100文字以内で入力してください', en: 'Name must be at most 100 characters' } },
|
|
32
|
+
],
|
|
33
|
+
email: [
|
|
34
|
+
{ required: true, message: { ja: 'メールアドレスは必須です', en: 'Email is required' } },
|
|
35
|
+
{ type: 'email', message: { ja: 'メールアドレスの形式が正しくありません', en: 'Email is not a valid email address' } },
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Helper functions
|
|
40
|
+
export function getUserRules(locale: string): Record<string, Rule[]>;
|
|
41
|
+
export function getUserDisplayName(locale: string): string;
|
|
42
|
+
export function getUserPropertyDisplayName(property: string, locale: string): string;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Basic Usage
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { Form, Input, Button } from 'antd';
|
|
49
|
+
import {
|
|
50
|
+
getUserRules,
|
|
51
|
+
getUserDisplayName,
|
|
52
|
+
getUserPropertyDisplayName
|
|
53
|
+
} from '@/types/model/rules/User.rules';
|
|
54
|
+
|
|
55
|
+
interface UserFormProps {
|
|
56
|
+
locale?: string;
|
|
57
|
+
onSubmit: (values: User) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function UserForm({ locale = 'ja', onSubmit }: UserFormProps) {
|
|
61
|
+
const [form] = Form.useForm();
|
|
62
|
+
const rules = getUserRules(locale);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Form
|
|
66
|
+
form={form}
|
|
67
|
+
layout="vertical"
|
|
68
|
+
onFinish={onSubmit}
|
|
69
|
+
>
|
|
70
|
+
<Form.Item
|
|
71
|
+
name="name"
|
|
72
|
+
label={getUserPropertyDisplayName('name', locale)}
|
|
73
|
+
rules={rules.name}
|
|
74
|
+
>
|
|
75
|
+
<Input placeholder={getUserPropertyDisplayName('name', locale)} />
|
|
76
|
+
</Form.Item>
|
|
77
|
+
|
|
78
|
+
<Form.Item
|
|
79
|
+
name="email"
|
|
80
|
+
label={getUserPropertyDisplayName('email', locale)}
|
|
81
|
+
rules={rules.email}
|
|
82
|
+
>
|
|
83
|
+
<Input type="email" />
|
|
84
|
+
</Form.Item>
|
|
85
|
+
|
|
86
|
+
<Form.Item>
|
|
87
|
+
<Button type="primary" htmlType="submit">
|
|
88
|
+
{locale === 'ja' ? '送信' : 'Submit'}
|
|
89
|
+
</Button>
|
|
90
|
+
</Form.Item>
|
|
91
|
+
</Form>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## With Edit Mode (Initial Values)
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { Form, Input, Button, Spin } from 'antd';
|
|
100
|
+
import { getUserRules, getUserPropertyDisplayName } from '@/types/model/rules/User.rules';
|
|
101
|
+
import { User } from '@/types/model';
|
|
102
|
+
|
|
103
|
+
interface UserEditFormProps {
|
|
104
|
+
user: User;
|
|
105
|
+
locale?: string;
|
|
106
|
+
onSubmit: (values: Partial<User>) => void;
|
|
107
|
+
loading?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function UserEditForm({ user, locale = 'ja', onSubmit, loading }: UserEditFormProps) {
|
|
111
|
+
const [form] = Form.useForm();
|
|
112
|
+
const rules = getUserRules(locale);
|
|
113
|
+
|
|
114
|
+
// Set initial values when user data changes
|
|
115
|
+
React.useEffect(() => {
|
|
116
|
+
form.setFieldsValue(user);
|
|
117
|
+
}, [user, form]);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Spin spinning={loading}>
|
|
121
|
+
<Form
|
|
122
|
+
form={form}
|
|
123
|
+
layout="vertical"
|
|
124
|
+
initialValues={user}
|
|
125
|
+
onFinish={onSubmit}
|
|
126
|
+
>
|
|
127
|
+
<Form.Item
|
|
128
|
+
name="name"
|
|
129
|
+
label={getUserPropertyDisplayName('name', locale)}
|
|
130
|
+
rules={rules.name}
|
|
131
|
+
>
|
|
132
|
+
<Input />
|
|
133
|
+
</Form.Item>
|
|
134
|
+
|
|
135
|
+
<Form.Item
|
|
136
|
+
name="email"
|
|
137
|
+
label={getUserPropertyDisplayName('email', locale)}
|
|
138
|
+
rules={rules.email}
|
|
139
|
+
>
|
|
140
|
+
<Input type="email" />
|
|
141
|
+
</Form.Item>
|
|
142
|
+
|
|
143
|
+
<Form.Item>
|
|
144
|
+
<Button type="primary" htmlType="submit" loading={loading}>
|
|
145
|
+
{locale === 'ja' ? '更新' : 'Update'}
|
|
146
|
+
</Button>
|
|
147
|
+
</Form.Item>
|
|
148
|
+
</Form>
|
|
149
|
+
</Spin>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Dynamic Locale Switching
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { Form, Input, Select } from 'antd';
|
|
158
|
+
import { useState, useMemo } from 'react';
|
|
159
|
+
import { getUserRules, getUserPropertyDisplayName, getUserDisplayName } from '@/types/model/rules/User.rules';
|
|
160
|
+
|
|
161
|
+
export function UserFormWithLocale() {
|
|
162
|
+
const [locale, setLocale] = useState('ja');
|
|
163
|
+
const [form] = Form.useForm();
|
|
164
|
+
|
|
165
|
+
// Memoize rules to avoid recalculation
|
|
166
|
+
const rules = useMemo(() => getUserRules(locale), [locale]);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<>
|
|
170
|
+
<Select value={locale} onChange={setLocale} style={{ marginBottom: 16 }}>
|
|
171
|
+
<Select.Option value="ja">日本語</Select.Option>
|
|
172
|
+
<Select.Option value="en">English</Select.Option>
|
|
173
|
+
<Select.Option value="vi">Tiếng Việt</Select.Option>
|
|
174
|
+
</Select>
|
|
175
|
+
|
|
176
|
+
<h2>{getUserDisplayName(locale)}</h2>
|
|
177
|
+
|
|
178
|
+
<Form form={form} layout="vertical">
|
|
179
|
+
<Form.Item
|
|
180
|
+
name="name"
|
|
181
|
+
label={getUserPropertyDisplayName('name', locale)}
|
|
182
|
+
rules={rules.name}
|
|
183
|
+
>
|
|
184
|
+
<Input />
|
|
185
|
+
</Form.Item>
|
|
186
|
+
|
|
187
|
+
<Form.Item
|
|
188
|
+
name="email"
|
|
189
|
+
label={getUserPropertyDisplayName('email', locale)}
|
|
190
|
+
rules={rules.email}
|
|
191
|
+
>
|
|
192
|
+
<Input type="email" />
|
|
193
|
+
</Form.Item>
|
|
194
|
+
</Form>
|
|
195
|
+
</>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## With Table Columns
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
import { Table, TableColumnsType } from 'antd';
|
|
204
|
+
import { getUserPropertyDisplayName } from '@/types/model/rules/User.rules';
|
|
205
|
+
import { User } from '@/types/model';
|
|
206
|
+
|
|
207
|
+
interface UserTableProps {
|
|
208
|
+
users: User[];
|
|
209
|
+
locale?: string;
|
|
210
|
+
loading?: boolean;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function UserTable({ users, locale = 'ja', loading }: UserTableProps) {
|
|
214
|
+
const columns: TableColumnsType<User> = [
|
|
215
|
+
{
|
|
216
|
+
title: getUserPropertyDisplayName('name', locale),
|
|
217
|
+
dataIndex: 'name',
|
|
218
|
+
key: 'name',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
title: getUserPropertyDisplayName('email', locale),
|
|
222
|
+
dataIndex: 'email',
|
|
223
|
+
key: 'email',
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<Table
|
|
229
|
+
columns={columns}
|
|
230
|
+
dataSource={users}
|
|
231
|
+
rowKey="id"
|
|
232
|
+
loading={loading}
|
|
233
|
+
/>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Custom Form Hook
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
import { Form, FormInstance } from 'antd';
|
|
242
|
+
import { useMemo } from 'react';
|
|
243
|
+
|
|
244
|
+
// Generic hook for any model's rules
|
|
245
|
+
export function useModelForm<T>(
|
|
246
|
+
getRules: (locale: string) => Record<string, any[]>,
|
|
247
|
+
locale: string = 'ja'
|
|
248
|
+
): {
|
|
249
|
+
form: FormInstance<T>;
|
|
250
|
+
rules: Record<string, any[]>;
|
|
251
|
+
} {
|
|
252
|
+
const [form] = Form.useForm<T>();
|
|
253
|
+
const rules = useMemo(() => getRules(locale), [getRules, locale]);
|
|
254
|
+
|
|
255
|
+
return { form, rules };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Usage
|
|
259
|
+
import { getUserRules } from '@/types/model/rules/User.rules';
|
|
260
|
+
|
|
261
|
+
function MyComponent() {
|
|
262
|
+
const { form, rules } = useModelForm(getUserRules, 'ja');
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<Form form={form}>
|
|
266
|
+
<Form.Item name="name" rules={rules.name}>
|
|
267
|
+
<Input />
|
|
268
|
+
</Form.Item>
|
|
269
|
+
</Form>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Modal Form
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
import { Modal, Form, Input, message } from 'antd';
|
|
278
|
+
import { getUserRules, getUserDisplayName, getUserPropertyDisplayName } from '@/types/model/rules/User.rules';
|
|
279
|
+
import { User } from '@/types/model';
|
|
280
|
+
|
|
281
|
+
interface UserModalFormProps {
|
|
282
|
+
open: boolean;
|
|
283
|
+
onClose: () => void;
|
|
284
|
+
onSubmit: (values: Partial<User>) => Promise<void>;
|
|
285
|
+
initialValues?: Partial<User>;
|
|
286
|
+
locale?: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function UserModalForm({
|
|
290
|
+
open,
|
|
291
|
+
onClose,
|
|
292
|
+
onSubmit,
|
|
293
|
+
initialValues,
|
|
294
|
+
locale = 'ja'
|
|
295
|
+
}: UserModalFormProps) {
|
|
296
|
+
const [form] = Form.useForm();
|
|
297
|
+
const [loading, setLoading] = useState(false);
|
|
298
|
+
const rules = getUserRules(locale);
|
|
299
|
+
|
|
300
|
+
const handleSubmit = async () => {
|
|
301
|
+
try {
|
|
302
|
+
const values = await form.validateFields();
|
|
303
|
+
setLoading(true);
|
|
304
|
+
await onSubmit(values);
|
|
305
|
+
message.success(locale === 'ja' ? '保存しました' : 'Saved successfully');
|
|
306
|
+
form.resetFields();
|
|
307
|
+
onClose();
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error instanceof Error) {
|
|
310
|
+
message.error(error.message);
|
|
311
|
+
}
|
|
312
|
+
} finally {
|
|
313
|
+
setLoading(false);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<Modal
|
|
319
|
+
title={getUserDisplayName(locale)}
|
|
320
|
+
open={open}
|
|
321
|
+
onCancel={onClose}
|
|
322
|
+
onOk={handleSubmit}
|
|
323
|
+
confirmLoading={loading}
|
|
324
|
+
okText={locale === 'ja' ? '保存' : 'Save'}
|
|
325
|
+
cancelText={locale === 'ja' ? 'キャンセル' : 'Cancel'}
|
|
326
|
+
>
|
|
327
|
+
<Form
|
|
328
|
+
form={form}
|
|
329
|
+
layout="vertical"
|
|
330
|
+
initialValues={initialValues}
|
|
331
|
+
>
|
|
332
|
+
<Form.Item
|
|
333
|
+
name="name"
|
|
334
|
+
label={getUserPropertyDisplayName('name', locale)}
|
|
335
|
+
rules={rules.name}
|
|
336
|
+
>
|
|
337
|
+
<Input />
|
|
338
|
+
</Form.Item>
|
|
339
|
+
|
|
340
|
+
<Form.Item
|
|
341
|
+
name="email"
|
|
342
|
+
label={getUserPropertyDisplayName('email', locale)}
|
|
343
|
+
rules={rules.email}
|
|
344
|
+
>
|
|
345
|
+
<Input type="email" />
|
|
346
|
+
</Form.Item>
|
|
347
|
+
</Form>
|
|
348
|
+
</Modal>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Validation Rule Types
|
|
354
|
+
|
|
355
|
+
Omnify generates these rule types based on schema:
|
|
356
|
+
|
|
357
|
+
| Schema | Generated Rule |
|
|
358
|
+
|--------|---------------|
|
|
359
|
+
| `required: true` | `{ required: true, message: {...} }` |
|
|
360
|
+
| `type: Email` | `{ type: 'email', message: {...} }` |
|
|
361
|
+
| `maxLength: N` | `{ max: N, message: {...} }` |
|
|
362
|
+
| `minLength: N` | `{ min: N, message: {...} }` |
|
|
363
|
+
| `max: N` (numeric) | `{ max: N, type: 'number', message: {...} }` |
|
|
364
|
+
| `min: N` (numeric) | `{ min: N, type: 'number', message: {...} }` |
|
|
365
|
+
| `pattern: regex` | `{ pattern: /regex/, message: {...} }` |
|
|
366
|
+
|
|
367
|
+
## Built-in Validation Messages
|
|
368
|
+
|
|
369
|
+
Omnify includes templates for 5 languages:
|
|
370
|
+
- Japanese (ja)
|
|
371
|
+
- English (en)
|
|
372
|
+
- Vietnamese (vi)
|
|
373
|
+
- Korean (ko)
|
|
374
|
+
- Chinese (zh)
|
|
375
|
+
|
|
376
|
+
Custom templates can be configured in `omnify.config.ts`:
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
export default defineConfig({
|
|
380
|
+
output: {
|
|
381
|
+
typescript: {
|
|
382
|
+
validationTemplates: {
|
|
383
|
+
required: {
|
|
384
|
+
ja: '${displayName}を入力してください',
|
|
385
|
+
en: '${displayName} is required',
|
|
386
|
+
vi: '${displayName} là bắt buộc',
|
|
387
|
+
},
|
|
388
|
+
maxLength: {
|
|
389
|
+
ja: '${displayName}は${max}文字以内です',
|
|
390
|
+
en: '${displayName} must be at most ${max} characters',
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Available placeholders:
|
|
399
|
+
- `${displayName}` - Property display name
|
|
400
|
+
- `${min}` - Minimum value/length
|
|
401
|
+
- `${max}` - Maximum value/length
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Omnify TypeScript Generator Guide
|
|
2
|
+
|
|
3
|
+
This guide covers TypeScript-specific features and generated code patterns for Omnify.
|
|
4
|
+
|
|
5
|
+
## Generated Files
|
|
6
|
+
|
|
7
|
+
When you run `npx omnify generate`, the following TypeScript files are generated:
|
|
8
|
+
|
|
9
|
+
- `base/*.ts` - Base model interfaces
|
|
10
|
+
- `enum/*.ts` - Enum types with multi-locale labels
|
|
11
|
+
- `rules/*.ts` - Ant Design compatible validation rules
|
|
12
|
+
|
|
13
|
+
## Type Generation
|
|
14
|
+
|
|
15
|
+
### Object Schema → Interface
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
# yaml-language-server: $schema=./node_modules/.omnify/combined-schema.json
|
|
19
|
+
name: User
|
|
20
|
+
properties:
|
|
21
|
+
id:
|
|
22
|
+
type: BigInt
|
|
23
|
+
required: true
|
|
24
|
+
name:
|
|
25
|
+
type: String
|
|
26
|
+
required: true
|
|
27
|
+
maxLength: 255
|
|
28
|
+
email:
|
|
29
|
+
type: String
|
|
30
|
+
required: true
|
|
31
|
+
unique: true
|
|
32
|
+
profile:
|
|
33
|
+
type: Json
|
|
34
|
+
createdAt:
|
|
35
|
+
type: DateTime
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Generated:
|
|
39
|
+
```typescript
|
|
40
|
+
export interface User {
|
|
41
|
+
id: number;
|
|
42
|
+
name: string;
|
|
43
|
+
email: string;
|
|
44
|
+
profile: Record<string, unknown> | null;
|
|
45
|
+
createdAt: Date | null;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Type Mapping
|
|
50
|
+
|
|
51
|
+
| Schema Type | TypeScript Type |
|
|
52
|
+
|-------------|-----------------|
|
|
53
|
+
| `String` | `string` |
|
|
54
|
+
| `LongText` | `string` |
|
|
55
|
+
| `Int` | `number` |
|
|
56
|
+
| `BigInt` | `number` |
|
|
57
|
+
| `Float` | `number` |
|
|
58
|
+
| `Boolean` | `boolean` |
|
|
59
|
+
| `Date` | `Date` |
|
|
60
|
+
| `DateTime` | `Date` |
|
|
61
|
+
| `Json` | `Record<string, unknown>` |
|
|
62
|
+
| `EnumRef` | Generated enum type |
|
|
63
|
+
| `Association` | Related model type / array |
|
|
64
|
+
|
|
65
|
+
## Enum Generation (Multi-locale)
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
# schemas/PostStatus.yaml
|
|
69
|
+
name: PostStatus
|
|
70
|
+
kind: enum
|
|
71
|
+
displayName:
|
|
72
|
+
ja: 投稿ステータス
|
|
73
|
+
en: Post Status
|
|
74
|
+
values:
|
|
75
|
+
draft:
|
|
76
|
+
ja: 下書き
|
|
77
|
+
en: Draft
|
|
78
|
+
published:
|
|
79
|
+
ja: 公開済み
|
|
80
|
+
en: Published
|
|
81
|
+
archived:
|
|
82
|
+
ja: アーカイブ
|
|
83
|
+
en: Archived
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Generated:
|
|
87
|
+
```typescript
|
|
88
|
+
export const PostStatus = {
|
|
89
|
+
draft: 'draft',
|
|
90
|
+
published: 'published',
|
|
91
|
+
archived: 'archived',
|
|
92
|
+
} as const;
|
|
93
|
+
|
|
94
|
+
export type PostStatus = typeof PostStatus[keyof typeof PostStatus];
|
|
95
|
+
|
|
96
|
+
// Multi-locale labels
|
|
97
|
+
export const PostStatusLabels: Record<PostStatus, Record<string, string>> = {
|
|
98
|
+
draft: { ja: '下書き', en: 'Draft' },
|
|
99
|
+
published: { ja: '公開済み', en: 'Published' },
|
|
100
|
+
archived: { ja: 'アーカイブ', en: 'Archived' },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Get label for specific locale
|
|
104
|
+
export function getPostStatusLabel(value: PostStatus, locale: string = 'en'): string {
|
|
105
|
+
return PostStatusLabels[value]?.[locale] ?? PostStatusLabels[value]?.['en'] ?? value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Helper functions
|
|
109
|
+
export const PostStatusValues = Object.values(PostStatus);
|
|
110
|
+
export function isPostStatus(value: unknown): value is PostStatus {
|
|
111
|
+
return PostStatusValues.includes(value as PostStatus);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Validation Rules (Ant Design)
|
|
116
|
+
|
|
117
|
+
Omnify generates Ant Design compatible validation rules in `rules/` directory.
|
|
118
|
+
|
|
119
|
+
**See detailed guide:** `.claude/omnify/antdesign-guide.md`
|
|
120
|
+
|
|
121
|
+
Quick example:
|
|
122
|
+
```tsx
|
|
123
|
+
import { Form, Input } from 'antd';
|
|
124
|
+
import { getUserRules, getUserPropertyDisplayName } from './types/model/rules/User.rules';
|
|
125
|
+
|
|
126
|
+
function UserForm({ locale = 'ja' }) {
|
|
127
|
+
const rules = getUserRules(locale);
|
|
128
|
+
return (
|
|
129
|
+
<Form>
|
|
130
|
+
<Form.Item name="name" label={getUserPropertyDisplayName('name', locale)} rules={rules.name}>
|
|
131
|
+
<Input />
|
|
132
|
+
</Form.Item>
|
|
133
|
+
</Form>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Association Types
|
|
139
|
+
|
|
140
|
+
### ManyToOne
|
|
141
|
+
```yaml
|
|
142
|
+
author:
|
|
143
|
+
type: Association
|
|
144
|
+
relation: ManyToOne
|
|
145
|
+
target: User
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Generated:
|
|
149
|
+
```typescript
|
|
150
|
+
export interface Post {
|
|
151
|
+
authorId: number;
|
|
152
|
+
author?: User; // Optional: loaded relation
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### OneToMany
|
|
157
|
+
```yaml
|
|
158
|
+
posts:
|
|
159
|
+
type: Association
|
|
160
|
+
relation: OneToMany
|
|
161
|
+
target: Post
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Generated:
|
|
165
|
+
```typescript
|
|
166
|
+
export interface User {
|
|
167
|
+
posts?: Post[]; // Optional: loaded relation array
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### ManyToMany
|
|
172
|
+
```yaml
|
|
173
|
+
tags:
|
|
174
|
+
type: Association
|
|
175
|
+
relation: ManyToMany
|
|
176
|
+
target: Tag
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Generated:
|
|
180
|
+
```typescript
|
|
181
|
+
export interface Post {
|
|
182
|
+
tags?: Tag[]; // Optional: loaded relation array
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Nullable Fields
|
|
187
|
+
|
|
188
|
+
Fields without `required: true` are nullable:
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
description:
|
|
192
|
+
type: LongText # No required: true
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Generated:
|
|
196
|
+
```typescript
|
|
197
|
+
description: string | null;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Using Generated Types
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { User, Post, PostStatus, isPostStatus } from './types/omnify-types';
|
|
204
|
+
|
|
205
|
+
// Type-safe object creation
|
|
206
|
+
const user: User = {
|
|
207
|
+
id: 1,
|
|
208
|
+
name: 'John',
|
|
209
|
+
email: 'john@example.com',
|
|
210
|
+
profile: null,
|
|
211
|
+
createdAt: new Date(),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Enum usage
|
|
215
|
+
const status: PostStatus = PostStatus.draft;
|
|
216
|
+
|
|
217
|
+
// Type guard
|
|
218
|
+
function handleStatus(value: unknown) {
|
|
219
|
+
if (isPostStatus(value)) {
|
|
220
|
+
console.log('Valid status:', value);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Commands
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# Generate TypeScript types
|
|
229
|
+
npx omnify generate
|
|
230
|
+
|
|
231
|
+
# Force regenerate all files
|
|
232
|
+
npx omnify generate --force
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Configuration
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// omnify.config.ts
|
|
239
|
+
import { defineConfig } from '@famgia/omnify';
|
|
240
|
+
|
|
241
|
+
export default defineConfig({
|
|
242
|
+
schemasDir: './schemas',
|
|
243
|
+
output: {
|
|
244
|
+
typescript: {
|
|
245
|
+
path: './src/types/model',
|
|
246
|
+
generateRules: true, // Generate Ant Design validation rules
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
locale: {
|
|
250
|
+
locales: ['ja', 'en'],
|
|
251
|
+
defaultLocale: 'ja',
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@famgia/omnify-typescript",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.29",
|
|
4
4
|
"description": "TypeScript type definitions generator for Omnify schemas",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"dist",
|
|
33
|
-
"scripts"
|
|
33
|
+
"scripts",
|
|
34
|
+
"ai-guides"
|
|
34
35
|
],
|
|
35
36
|
"keywords": [
|
|
36
37
|
"omnify",
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
"directory": "packages/typescript-generator"
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
50
|
-
"@famgia/omnify-types": "0.0.
|
|
51
|
+
"@famgia/omnify-types": "0.0.37"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
54
|
"tsup": "^8.5.1",
|
package/scripts/postinstall.js
CHANGED
|
@@ -2,228 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
This guide covers TypeScript-specific features and generated code patterns for Omnify.
|
|
9
|
-
|
|
10
|
-
## Generated Files
|
|
11
|
-
|
|
12
|
-
When you run \`npx omnify generate\`, the following TypeScript files are generated:
|
|
13
|
-
|
|
14
|
-
- \`types/omnify-types.ts\` - All type definitions
|
|
15
|
-
- \`types/enums.ts\` - Enum types and helpers
|
|
16
|
-
|
|
17
|
-
## Type Generation
|
|
18
|
-
|
|
19
|
-
### Object Schema → Interface
|
|
20
|
-
|
|
21
|
-
\`\`\`yaml
|
|
22
|
-
# yaml-language-server: $schema=./node_modules/.omnify/combined-schema.json
|
|
23
|
-
name: User
|
|
24
|
-
properties:
|
|
25
|
-
id:
|
|
26
|
-
type: BigInt
|
|
27
|
-
required: true
|
|
28
|
-
name:
|
|
29
|
-
type: String
|
|
30
|
-
required: true
|
|
31
|
-
maxLength: 255
|
|
32
|
-
email:
|
|
33
|
-
type: String
|
|
34
|
-
required: true
|
|
35
|
-
unique: true
|
|
36
|
-
profile:
|
|
37
|
-
type: Json
|
|
38
|
-
createdAt:
|
|
39
|
-
type: DateTime
|
|
40
|
-
\`\`\`
|
|
41
|
-
|
|
42
|
-
Generated:
|
|
43
|
-
\`\`\`typescript
|
|
44
|
-
export interface User {
|
|
45
|
-
id: number;
|
|
46
|
-
name: string;
|
|
47
|
-
email: string;
|
|
48
|
-
profile: Record<string, unknown> | null;
|
|
49
|
-
createdAt: Date | null;
|
|
50
|
-
}
|
|
51
|
-
\`\`\`
|
|
52
|
-
|
|
53
|
-
## Type Mapping
|
|
54
|
-
|
|
55
|
-
| Schema Type | TypeScript Type |
|
|
56
|
-
|-------------|-----------------|
|
|
57
|
-
| \`String\` | \`string\` |
|
|
58
|
-
| \`LongText\` | \`string\` |
|
|
59
|
-
| \`Int\` | \`number\` |
|
|
60
|
-
| \`BigInt\` | \`number\` |
|
|
61
|
-
| \`Float\` | \`number\` |
|
|
62
|
-
| \`Boolean\` | \`boolean\` |
|
|
63
|
-
| \`Date\` | \`Date\` |
|
|
64
|
-
| \`DateTime\` | \`Date\` |
|
|
65
|
-
| \`Json\` | \`Record<string, unknown>\` |
|
|
66
|
-
| \`EnumRef\` | Generated enum type |
|
|
67
|
-
| \`Association\` | Related model type / array |
|
|
68
|
-
|
|
69
|
-
## Enum Generation
|
|
70
|
-
|
|
71
|
-
\`\`\`yaml
|
|
72
|
-
# schemas/PostStatus.yaml
|
|
73
|
-
name: PostStatus
|
|
74
|
-
kind: enum
|
|
75
|
-
values:
|
|
76
|
-
draft: 下書き
|
|
77
|
-
published: 公開済み
|
|
78
|
-
archived: アーカイブ
|
|
79
|
-
\`\`\`
|
|
80
|
-
|
|
81
|
-
Generated:
|
|
82
|
-
\`\`\`typescript
|
|
83
|
-
export const PostStatus = {
|
|
84
|
-
draft: 'draft',
|
|
85
|
-
published: 'published',
|
|
86
|
-
archived: 'archived',
|
|
87
|
-
} as const;
|
|
88
|
-
|
|
89
|
-
export type PostStatus = typeof PostStatus[keyof typeof PostStatus];
|
|
90
|
-
|
|
91
|
-
// Helper functions
|
|
92
|
-
export const PostStatusValues = Object.values(PostStatus);
|
|
93
|
-
export const PostStatusKeys = Object.keys(PostStatus) as (keyof typeof PostStatus)[];
|
|
94
|
-
|
|
95
|
-
export function isPostStatus(value: unknown): value is PostStatus {
|
|
96
|
-
return PostStatusValues.includes(value as PostStatus);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Display names for UI
|
|
100
|
-
export const PostStatusDisplayNames: Record<PostStatus, string> = {
|
|
101
|
-
draft: '下書き',
|
|
102
|
-
published: '公開済み',
|
|
103
|
-
archived: 'アーカイブ',
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
export function getPostStatusDisplayName(value: PostStatus): string {
|
|
107
|
-
return PostStatusDisplayNames[value];
|
|
108
|
-
}
|
|
109
|
-
\`\`\`
|
|
110
|
-
|
|
111
|
-
## Association Types
|
|
112
|
-
|
|
113
|
-
### ManyToOne
|
|
114
|
-
\`\`\`yaml
|
|
115
|
-
author:
|
|
116
|
-
type: Association
|
|
117
|
-
relation: ManyToOne
|
|
118
|
-
target: User
|
|
119
|
-
\`\`\`
|
|
120
|
-
|
|
121
|
-
Generated:
|
|
122
|
-
\`\`\`typescript
|
|
123
|
-
export interface Post {
|
|
124
|
-
authorId: number;
|
|
125
|
-
author?: User; // Optional: loaded relation
|
|
126
|
-
}
|
|
127
|
-
\`\`\`
|
|
128
|
-
|
|
129
|
-
### OneToMany
|
|
130
|
-
\`\`\`yaml
|
|
131
|
-
posts:
|
|
132
|
-
type: Association
|
|
133
|
-
relation: OneToMany
|
|
134
|
-
target: Post
|
|
135
|
-
\`\`\`
|
|
136
|
-
|
|
137
|
-
Generated:
|
|
138
|
-
\`\`\`typescript
|
|
139
|
-
export interface User {
|
|
140
|
-
posts?: Post[]; // Optional: loaded relation array
|
|
141
|
-
}
|
|
142
|
-
\`\`\`
|
|
143
|
-
|
|
144
|
-
### ManyToMany
|
|
145
|
-
\`\`\`yaml
|
|
146
|
-
tags:
|
|
147
|
-
type: Association
|
|
148
|
-
relation: ManyToMany
|
|
149
|
-
target: Tag
|
|
150
|
-
\`\`\`
|
|
151
|
-
|
|
152
|
-
Generated:
|
|
153
|
-
\`\`\`typescript
|
|
154
|
-
export interface Post {
|
|
155
|
-
tags?: Tag[]; // Optional: loaded relation array
|
|
156
|
-
}
|
|
157
|
-
\`\`\`
|
|
158
|
-
|
|
159
|
-
## Nullable Fields
|
|
160
|
-
|
|
161
|
-
Fields without \`required: true\` are nullable:
|
|
162
|
-
|
|
163
|
-
\`\`\`yaml
|
|
164
|
-
description:
|
|
165
|
-
type: LongText # No required: true
|
|
166
|
-
\`\`\`
|
|
167
|
-
|
|
168
|
-
Generated:
|
|
169
|
-
\`\`\`typescript
|
|
170
|
-
description: string | null;
|
|
171
|
-
\`\`\`
|
|
172
|
-
|
|
173
|
-
## Using Generated Types
|
|
174
|
-
|
|
175
|
-
\`\`\`typescript
|
|
176
|
-
import { User, Post, PostStatus, isPostStatus } from './types/omnify-types';
|
|
177
|
-
|
|
178
|
-
// Type-safe object creation
|
|
179
|
-
const user: User = {
|
|
180
|
-
id: 1,
|
|
181
|
-
name: 'John',
|
|
182
|
-
email: 'john@example.com',
|
|
183
|
-
profile: null,
|
|
184
|
-
createdAt: new Date(),
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
// Enum usage
|
|
188
|
-
const status: PostStatus = PostStatus.draft;
|
|
189
|
-
|
|
190
|
-
// Type guard
|
|
191
|
-
function handleStatus(value: unknown) {
|
|
192
|
-
if (isPostStatus(value)) {
|
|
193
|
-
console.log('Valid status:', value);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
\`\`\`
|
|
197
|
-
|
|
198
|
-
## Commands
|
|
199
|
-
|
|
200
|
-
\`\`\`bash
|
|
201
|
-
# Generate TypeScript types
|
|
202
|
-
npx omnify generate --typescript
|
|
203
|
-
|
|
204
|
-
# Generate to specific output
|
|
205
|
-
npx omnify generate --typescript --output ./src/types
|
|
206
|
-
|
|
207
|
-
# Watch for changes
|
|
208
|
-
npx omnify watch --typescript
|
|
209
|
-
\`\`\`
|
|
210
|
-
|
|
211
|
-
## Configuration
|
|
212
|
-
|
|
213
|
-
\`\`\`javascript
|
|
214
|
-
// omnify.config.js
|
|
215
|
-
export default {
|
|
216
|
-
schemasDir: './schemas',
|
|
217
|
-
typescript: {
|
|
218
|
-
outputDir: './types',
|
|
219
|
-
outputFile: 'omnify-types.ts',
|
|
220
|
-
enumsFile: 'enums.ts',
|
|
221
|
-
generateHelpers: true, // Generate enum helpers
|
|
222
|
-
strictNullChecks: true // Use | null for optional
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
\`\`\`
|
|
226
|
-
`;
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
227
9
|
|
|
228
10
|
function findProjectRoot() {
|
|
229
11
|
// npm/pnpm set INIT_CWD to the directory where the install was run
|
|
@@ -239,17 +21,30 @@ function findProjectRoot() {
|
|
|
239
21
|
return null;
|
|
240
22
|
}
|
|
241
23
|
|
|
242
|
-
function
|
|
24
|
+
function copyAiGuidesToProject(projectRoot) {
|
|
243
25
|
const omnifyDir = path.join(projectRoot, '.claude', 'omnify');
|
|
26
|
+
const aiGuidesDir = path.join(__dirname, '..', 'ai-guides');
|
|
244
27
|
|
|
245
28
|
try {
|
|
29
|
+
// Create target directory
|
|
246
30
|
if (!fs.existsSync(omnifyDir)) {
|
|
247
31
|
fs.mkdirSync(omnifyDir, { recursive: true });
|
|
248
32
|
}
|
|
249
33
|
|
|
250
|
-
|
|
251
|
-
fs.
|
|
252
|
-
|
|
34
|
+
// Copy all files from ai-guides directory
|
|
35
|
+
if (fs.existsSync(aiGuidesDir)) {
|
|
36
|
+
const files = fs.readdirSync(aiGuidesDir);
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
const srcPath = path.join(aiGuidesDir, file);
|
|
39
|
+
const destPath = path.join(omnifyDir, file);
|
|
40
|
+
|
|
41
|
+
if (fs.statSync(srcPath).isFile()) {
|
|
42
|
+
fs.copyFileSync(srcPath, destPath);
|
|
43
|
+
console.log(` Created .claude/omnify/${file}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
253
48
|
return true;
|
|
254
49
|
} catch {
|
|
255
50
|
return false;
|
|
@@ -269,7 +64,7 @@ function main() {
|
|
|
269
64
|
|
|
270
65
|
const projectRoot = findProjectRoot();
|
|
271
66
|
if (projectRoot) {
|
|
272
|
-
|
|
67
|
+
copyAiGuidesToProject(projectRoot);
|
|
273
68
|
}
|
|
274
69
|
}
|
|
275
70
|
|