@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 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?.idType ?? 'BigInt';
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?.idType ?? 'BigInt');
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 idType = options.idType ?? 'BigInt';
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.
@@ -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
- rules.push(nullable ? 'nullable' : 'required');
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;
@@ -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
- const isNullable = propDef.nullable ?? false;
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
- if (isNullable && schema) {
132
- schema += '.optional().nullable()';
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/ts",
3
- "version": "0.3.7",
3
+ "version": "0.4.0",
4
4
  "description": "TypeScript model type generator from Omnify schemas.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",