@omnifyjp/ts 0.3.7 → 0.4.0
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/cli.js +0 -0
- package/dist/interface-generator.js +10 -2
- package/dist/php/model-generator.js +3 -2
- package/dist/php/type-mapper.js +92 -1
- package/dist/types.d.ts +31 -2
- package/dist/zod-generator.js +112 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
File without changes
|
|
@@ -40,8 +40,16 @@ const PK_TYPE_MAP = {
|
|
|
40
40
|
Int: 'number',
|
|
41
41
|
BigInt: 'number',
|
|
42
42
|
Uuid: 'string',
|
|
43
|
+
Ulid: 'string',
|
|
43
44
|
String: 'string',
|
|
44
45
|
};
|
|
46
|
+
/** Extract IdType from the unified `id` field. Returns 'BigInt' as default. */
|
|
47
|
+
function getIdType(options) {
|
|
48
|
+
const id = options?.id;
|
|
49
|
+
if (typeof id === 'string')
|
|
50
|
+
return id;
|
|
51
|
+
return 'BigInt';
|
|
52
|
+
}
|
|
45
53
|
/** Gets TypeScript type for a property. */
|
|
46
54
|
export function getPropertyType(property, allSchemas) {
|
|
47
55
|
if (property.type === 'File') {
|
|
@@ -168,7 +176,7 @@ export function propertyToTSProperties(propertyName, property, schema, allSchema
|
|
|
168
176
|
// Determine FK type from target schema
|
|
169
177
|
let fkType = 'number';
|
|
170
178
|
const targetSchema = property.target ? allSchemas[property.target] : undefined;
|
|
171
|
-
const effectiveIdType = targetSchema?.options
|
|
179
|
+
const effectiveIdType = getIdType(targetSchema?.options);
|
|
172
180
|
if (effectiveIdType === 'Uuid' || effectiveIdType === 'String') {
|
|
173
181
|
fkType = 'string';
|
|
174
182
|
}
|
|
@@ -226,7 +234,7 @@ export function schemaToInterface(schema, allSchemas, options) {
|
|
|
226
234
|
const allSchemaNames = new Set(Object.keys(allSchemas).filter(name => allSchemas[name]?.kind !== 'enum'));
|
|
227
235
|
// ID property
|
|
228
236
|
if (schema.options?.id !== false) {
|
|
229
|
-
const pkType = (schema.options
|
|
237
|
+
const pkType = getIdType(schema.options);
|
|
230
238
|
properties.push({
|
|
231
239
|
name: 'id',
|
|
232
240
|
type: PK_TYPE_MAP[pkType] ?? 'number',
|
|
@@ -42,9 +42,10 @@ function generateBaseModel(name, schema, reader, config) {
|
|
|
42
42
|
const relations = buildRelations(properties, propertyOrder, modelNamespace);
|
|
43
43
|
const accessors = buildAccessors(expandedProperties);
|
|
44
44
|
const primaryKey = options['primaryKey'] ?? 'id';
|
|
45
|
-
const
|
|
45
|
+
const rawId = options.id;
|
|
46
|
+
const idType = typeof rawId === 'string' ? rawId : 'BigInt';
|
|
46
47
|
let keyTypeSection = '';
|
|
47
|
-
if (idType === 'Uuid' || idType === 'String') {
|
|
48
|
+
if (idType === 'Uuid' || idType === 'Ulid' || idType === 'String') {
|
|
48
49
|
keyTypeSection = `
|
|
49
50
|
/**
|
|
50
51
|
* The type of the primary key.
|
package/dist/php/type-mapper.js
CHANGED
|
@@ -63,13 +63,25 @@ export function toPhpDocType(type, nullable = false) {
|
|
|
63
63
|
}
|
|
64
64
|
return nullable ? `${phpType}|null` : phpType;
|
|
65
65
|
}
|
|
66
|
+
/** Type categories for rule applicability. */
|
|
67
|
+
const STRING_TYPES = new Set([
|
|
68
|
+
'String', 'Email', 'Password', 'Text', 'MediumText', 'LongText',
|
|
69
|
+
'Slug', 'Url', 'Phone', 'Uuid',
|
|
70
|
+
]);
|
|
71
|
+
const NUMERIC_TYPES = new Set(['Int', 'BigInt', 'TinyInt', 'Float', 'Decimal']);
|
|
72
|
+
const ARRAY_TYPES = new Set(['Json']);
|
|
66
73
|
/** Generate validation rules for a property (Store request). */
|
|
67
74
|
export function toStoreRules(property, tableName) {
|
|
68
75
|
const type = property['type'] ?? 'String';
|
|
69
76
|
const nullable = property['nullable'] ?? false;
|
|
70
77
|
const unique = property['unique'] ?? false;
|
|
78
|
+
const vr = property['rules'];
|
|
71
79
|
const rules = [];
|
|
72
|
-
|
|
80
|
+
// required/nullable — rules.required can override
|
|
81
|
+
const isRequired = vr?.['required'] !== undefined
|
|
82
|
+
? vr['required']
|
|
83
|
+
: !nullable;
|
|
84
|
+
rules.push(isRequired ? 'required' : 'nullable');
|
|
73
85
|
switch (type) {
|
|
74
86
|
case 'String':
|
|
75
87
|
case 'Slug':
|
|
@@ -143,8 +155,87 @@ export function toStoreRules(property, tableName) {
|
|
|
143
155
|
if (unique && type !== 'Email') {
|
|
144
156
|
rules.push(`unique:${tableName}`);
|
|
145
157
|
}
|
|
158
|
+
// Merge explicit validation rules (additive/override)
|
|
159
|
+
if (vr) {
|
|
160
|
+
mergeValidationRules(rules, vr, type);
|
|
161
|
+
}
|
|
146
162
|
return rules;
|
|
147
163
|
}
|
|
164
|
+
/** Merge explicit ValidationRules into the inferred rules array. */
|
|
165
|
+
function mergeValidationRules(rules, vr, type) {
|
|
166
|
+
const isString = STRING_TYPES.has(type);
|
|
167
|
+
const isNumeric = NUMERIC_TYPES.has(type);
|
|
168
|
+
const isArray = ARRAY_TYPES.has(type);
|
|
169
|
+
if (isString) {
|
|
170
|
+
if (vr['minLength'] != null)
|
|
171
|
+
rules.push(`min:${vr['minLength']}`);
|
|
172
|
+
if (vr['maxLength'] != null) {
|
|
173
|
+
const idx = rules.findIndex(r => typeof r === 'string' && r.startsWith('max:'));
|
|
174
|
+
if (idx !== -1)
|
|
175
|
+
rules[idx] = `max:${vr['maxLength']}`;
|
|
176
|
+
else
|
|
177
|
+
rules.push(`max:${vr['maxLength']}`);
|
|
178
|
+
}
|
|
179
|
+
if (vr['url'] === true)
|
|
180
|
+
rules.push('url');
|
|
181
|
+
if (vr['uuid'] === true)
|
|
182
|
+
rules.push('uuid');
|
|
183
|
+
if (vr['ip'] === true)
|
|
184
|
+
rules.push('ip');
|
|
185
|
+
if (vr['ipv4'] === true)
|
|
186
|
+
rules.push('ipv4');
|
|
187
|
+
if (vr['ipv6'] === true)
|
|
188
|
+
rules.push('ipv6');
|
|
189
|
+
if (vr['alpha'] === true)
|
|
190
|
+
rules.push('alpha');
|
|
191
|
+
if (vr['alphaNum'] === true)
|
|
192
|
+
rules.push('alpha_num');
|
|
193
|
+
if (vr['alphaDash'] === true)
|
|
194
|
+
rules.push('alpha_dash');
|
|
195
|
+
if (vr['numeric'] === true)
|
|
196
|
+
rules.push('numeric');
|
|
197
|
+
if (vr['lowercase'] === true)
|
|
198
|
+
rules.push('lowercase');
|
|
199
|
+
if (vr['uppercase'] === true)
|
|
200
|
+
rules.push('uppercase');
|
|
201
|
+
if (vr['digits'] != null)
|
|
202
|
+
rules.push(`digits:${vr['digits']}`);
|
|
203
|
+
if (vr['digitsBetween'] != null) {
|
|
204
|
+
const [a, b] = vr['digitsBetween'];
|
|
205
|
+
rules.push(`digits_between:${a},${b}`);
|
|
206
|
+
}
|
|
207
|
+
if (vr['startsWith'] != null) {
|
|
208
|
+
const v = vr['startsWith'];
|
|
209
|
+
rules.push(`starts_with:${Array.isArray(v) ? v.join(',') : v}`);
|
|
210
|
+
}
|
|
211
|
+
if (vr['endsWith'] != null) {
|
|
212
|
+
const v = vr['endsWith'];
|
|
213
|
+
rules.push(`ends_with:${Array.isArray(v) ? v.join(',') : v}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (isNumeric) {
|
|
217
|
+
if (vr['min'] != null)
|
|
218
|
+
rules.push(`min:${vr['min']}`);
|
|
219
|
+
if (vr['max'] != null)
|
|
220
|
+
rules.push(`max:${vr['max']}`);
|
|
221
|
+
if (vr['between'] != null) {
|
|
222
|
+
const [a, b] = vr['between'];
|
|
223
|
+
rules.push(`between:${a},${b}`);
|
|
224
|
+
}
|
|
225
|
+
if (vr['gt'] != null)
|
|
226
|
+
rules.push(`gt:${vr['gt']}`);
|
|
227
|
+
if (vr['lt'] != null)
|
|
228
|
+
rules.push(`lt:${vr['lt']}`);
|
|
229
|
+
if (vr['multipleOf'] != null)
|
|
230
|
+
rules.push(`multiple_of:${vr['multipleOf']}`);
|
|
231
|
+
}
|
|
232
|
+
if (isArray) {
|
|
233
|
+
if (vr['arrayMin'] != null)
|
|
234
|
+
rules.push(`min:${vr['arrayMin']}`);
|
|
235
|
+
if (vr['arrayMax'] != null)
|
|
236
|
+
rules.push(`max:${vr['arrayMax']}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
148
239
|
/** Generate validation rules for a property (Update request). */
|
|
149
240
|
export function toUpdateRules(property, tableName, modelRouteParam) {
|
|
150
241
|
let rules = toStoreRules(property, tableName);
|
package/dist/types.d.ts
CHANGED
|
@@ -57,8 +57,7 @@ export interface ExpandedColumn {
|
|
|
57
57
|
}
|
|
58
58
|
/** Schema options. */
|
|
59
59
|
export interface SchemaOptions {
|
|
60
|
-
readonly id?: boolean;
|
|
61
|
-
readonly idType?: string;
|
|
60
|
+
readonly id?: boolean | string;
|
|
62
61
|
readonly timestamps?: boolean;
|
|
63
62
|
readonly softDelete?: boolean;
|
|
64
63
|
readonly hidden?: boolean;
|
|
@@ -81,6 +80,35 @@ export interface SchemaDefinition {
|
|
|
81
80
|
readonly values?: readonly EnumValueDefinition[];
|
|
82
81
|
readonly pivotFor?: readonly string[];
|
|
83
82
|
}
|
|
83
|
+
/** Application-level validation rules. Single source of truth for both Laravel validation and Zod schemas. */
|
|
84
|
+
export interface ValidationRules {
|
|
85
|
+
readonly required?: boolean;
|
|
86
|
+
readonly minLength?: number;
|
|
87
|
+
readonly maxLength?: number;
|
|
88
|
+
readonly url?: boolean;
|
|
89
|
+
readonly uuid?: boolean;
|
|
90
|
+
readonly ip?: boolean;
|
|
91
|
+
readonly ipv4?: boolean;
|
|
92
|
+
readonly ipv6?: boolean;
|
|
93
|
+
readonly alpha?: boolean;
|
|
94
|
+
readonly alphaNum?: boolean;
|
|
95
|
+
readonly alphaDash?: boolean;
|
|
96
|
+
readonly numeric?: boolean;
|
|
97
|
+
readonly digits?: number;
|
|
98
|
+
readonly digitsBetween?: readonly [number, number];
|
|
99
|
+
readonly startsWith?: string | readonly string[];
|
|
100
|
+
readonly endsWith?: string | readonly string[];
|
|
101
|
+
readonly lowercase?: boolean;
|
|
102
|
+
readonly uppercase?: boolean;
|
|
103
|
+
readonly min?: number;
|
|
104
|
+
readonly max?: number;
|
|
105
|
+
readonly between?: readonly [number, number];
|
|
106
|
+
readonly gt?: number;
|
|
107
|
+
readonly lt?: number;
|
|
108
|
+
readonly multipleOf?: number;
|
|
109
|
+
readonly arrayMin?: number;
|
|
110
|
+
readonly arrayMax?: number;
|
|
111
|
+
}
|
|
84
112
|
/** Property definition within a schema. */
|
|
85
113
|
export interface PropertyDefinition {
|
|
86
114
|
readonly type: string;
|
|
@@ -100,6 +128,7 @@ export interface PropertyDefinition {
|
|
|
100
128
|
readonly precision?: number;
|
|
101
129
|
readonly scale?: number;
|
|
102
130
|
readonly pattern?: string;
|
|
131
|
+
readonly rules?: ValidationRules;
|
|
103
132
|
readonly enum?: string | readonly string[];
|
|
104
133
|
readonly relation?: string;
|
|
105
134
|
readonly target?: string;
|
package/dist/zod-generator.js
CHANGED
|
@@ -26,11 +26,112 @@ function getMultiLocaleDisplayName(value, locales, fallbackLocale, defaultValue)
|
|
|
26
26
|
}
|
|
27
27
|
return result;
|
|
28
28
|
}
|
|
29
|
+
/** Type categories for Zod rule applicability. */
|
|
30
|
+
const ZOD_STRING_TYPES = new Set([
|
|
31
|
+
'String', 'Email', 'Password', 'Text', 'MediumText', 'LongText',
|
|
32
|
+
'Slug', 'Url', 'Phone', 'Uuid',
|
|
33
|
+
]);
|
|
34
|
+
const ZOD_NUMERIC_TYPES = new Set(['Int', 'BigInt', 'TinyInt', 'Float', 'Decimal']);
|
|
35
|
+
/** Replace an existing Zod method call or append it. */
|
|
36
|
+
function replaceZodMethod(schema, methodName, replacement) {
|
|
37
|
+
const regex = new RegExp(`\\.${methodName}\\([^)]*\\)`);
|
|
38
|
+
if (regex.test(schema)) {
|
|
39
|
+
return schema.replace(regex, replacement);
|
|
40
|
+
}
|
|
41
|
+
return schema + replacement;
|
|
42
|
+
}
|
|
43
|
+
/** Apply validation rules to a Zod schema string (additive/override). */
|
|
44
|
+
function applyZodRules(schema, rules, type) {
|
|
45
|
+
const isString = ZOD_STRING_TYPES.has(type);
|
|
46
|
+
const isNumeric = ZOD_NUMERIC_TYPES.has(type);
|
|
47
|
+
if (isString) {
|
|
48
|
+
if (rules.minLength != null) {
|
|
49
|
+
schema = replaceZodMethod(schema, 'min', `.min(${rules.minLength})`);
|
|
50
|
+
}
|
|
51
|
+
if (rules.maxLength != null) {
|
|
52
|
+
schema = replaceZodMethod(schema, 'max', `.max(${rules.maxLength})`);
|
|
53
|
+
}
|
|
54
|
+
if (rules.url)
|
|
55
|
+
schema += '.url()';
|
|
56
|
+
if (rules.uuid)
|
|
57
|
+
schema += '.uuid()';
|
|
58
|
+
if (rules.ip)
|
|
59
|
+
schema += '.ip()';
|
|
60
|
+
if (rules.ipv4)
|
|
61
|
+
schema += `.ip({ version: "v4" })`;
|
|
62
|
+
if (rules.ipv6)
|
|
63
|
+
schema += `.ip({ version: "v6" })`;
|
|
64
|
+
if (rules.alpha)
|
|
65
|
+
schema += `.regex(/^[a-zA-Z]+$/)`;
|
|
66
|
+
if (rules.alphaNum)
|
|
67
|
+
schema += `.regex(/^[a-zA-Z0-9]+$/)`;
|
|
68
|
+
if (rules.alphaDash)
|
|
69
|
+
schema += `.regex(/^[a-zA-Z0-9_-]+$/)`;
|
|
70
|
+
if (rules.numeric)
|
|
71
|
+
schema += `.regex(/^[0-9]+$/)`;
|
|
72
|
+
if (rules.lowercase)
|
|
73
|
+
schema += `.regex(/^[^A-Z]*$/)`;
|
|
74
|
+
if (rules.uppercase)
|
|
75
|
+
schema += `.regex(/^[^a-z]*$/)`;
|
|
76
|
+
if (rules.digits != null) {
|
|
77
|
+
schema += `.length(${rules.digits}).regex(/^[0-9]+$/)`;
|
|
78
|
+
}
|
|
79
|
+
if (rules.digitsBetween != null) {
|
|
80
|
+
const [a, b] = rules.digitsBetween;
|
|
81
|
+
schema = replaceZodMethod(schema, 'min', `.min(${a})`);
|
|
82
|
+
schema = replaceZodMethod(schema, 'max', `.max(${b})`);
|
|
83
|
+
schema += `.regex(/^[0-9]+$/)`;
|
|
84
|
+
}
|
|
85
|
+
if (rules.startsWith != null) {
|
|
86
|
+
const v = rules.startsWith;
|
|
87
|
+
if (Array.isArray(v)) {
|
|
88
|
+
const escaped = v.map(s => s.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'));
|
|
89
|
+
schema += `.regex(/^(${escaped.join('|')})/)`;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
schema += `.startsWith('${v}')`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (rules.endsWith != null) {
|
|
96
|
+
const v = rules.endsWith;
|
|
97
|
+
if (Array.isArray(v)) {
|
|
98
|
+
const escaped = v.map(s => s.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'));
|
|
99
|
+
schema += `.regex(/(${escaped.join('|')})$/)`;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
schema += `.endsWith('${v}')`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (isNumeric) {
|
|
107
|
+
if (rules.min != null) {
|
|
108
|
+
schema = replaceZodMethod(schema, 'gte', `.gte(${rules.min})`);
|
|
109
|
+
}
|
|
110
|
+
if (rules.max != null) {
|
|
111
|
+
schema = replaceZodMethod(schema, 'lte', `.lte(${rules.max})`);
|
|
112
|
+
}
|
|
113
|
+
if (rules.between != null) {
|
|
114
|
+
const [a, b] = rules.between;
|
|
115
|
+
schema = replaceZodMethod(schema, 'gte', `.gte(${a})`);
|
|
116
|
+
schema = replaceZodMethod(schema, 'lte', `.lte(${b})`);
|
|
117
|
+
}
|
|
118
|
+
if (rules.gt != null)
|
|
119
|
+
schema += `.gt(${rules.gt})`;
|
|
120
|
+
if (rules.lt != null)
|
|
121
|
+
schema += `.lt(${rules.lt})`;
|
|
122
|
+
if (rules.multipleOf != null)
|
|
123
|
+
schema += `.multipleOf(${rules.multipleOf})`;
|
|
124
|
+
}
|
|
125
|
+
return schema;
|
|
126
|
+
}
|
|
29
127
|
/**
|
|
30
128
|
* Generate Zod schema string for a property type.
|
|
31
129
|
*/
|
|
32
130
|
function getZodSchemaForType(propDef, _fieldName, options) {
|
|
33
|
-
|
|
131
|
+
// rules.required overrides nullable inference
|
|
132
|
+
const isNullable = propDef.rules?.required !== undefined
|
|
133
|
+
? !propDef.rules.required
|
|
134
|
+
: (propDef.nullable ?? false);
|
|
34
135
|
let schema = '';
|
|
35
136
|
// Check for simple custom types
|
|
36
137
|
const simpleType = options.customTypes.simple[propDef.type];
|
|
@@ -39,6 +140,9 @@ function getZodSchemaForType(propDef, _fieldName, options) {
|
|
|
39
140
|
if (simpleType.length) {
|
|
40
141
|
schema += `.max(${simpleType.length})`;
|
|
41
142
|
}
|
|
143
|
+
if (propDef.rules) {
|
|
144
|
+
schema = applyZodRules(schema, propDef.rules, propDef.type);
|
|
145
|
+
}
|
|
42
146
|
if (isNullable) {
|
|
43
147
|
schema += '.optional().nullable()';
|
|
44
148
|
}
|
|
@@ -128,12 +232,17 @@ function getZodSchemaForType(propDef, _fieldName, options) {
|
|
|
128
232
|
default:
|
|
129
233
|
schema = 'z.string()';
|
|
130
234
|
}
|
|
131
|
-
|
|
132
|
-
|
|
235
|
+
// Apply validation rules (additive/override)
|
|
236
|
+
if (propDef.rules && schema) {
|
|
237
|
+
schema = applyZodRules(schema, propDef.rules, propDef.type);
|
|
133
238
|
}
|
|
239
|
+
// Pattern (before nullable so validators chain correctly)
|
|
134
240
|
if (propDef.pattern && schema) {
|
|
135
241
|
schema += `.regex(/${propDef.pattern}/)`;
|
|
136
242
|
}
|
|
243
|
+
if (isNullable && schema) {
|
|
244
|
+
schema += '.optional().nullable()';
|
|
245
|
+
}
|
|
137
246
|
return schema;
|
|
138
247
|
}
|
|
139
248
|
/**
|