@omnifyjp/omnify 3.13.0 → 3.13.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/omnify",
3
- "version": "3.13.0",
3
+ "version": "3.13.1",
4
4
  "description": "Schema-driven code generation for Laravel, TypeScript, and SQL",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,10 +36,10 @@
36
36
  "zod": "^3.24.0"
37
37
  },
38
38
  "optionalDependencies": {
39
- "@omnifyjp/omnify-darwin-arm64": "3.13.0",
40
- "@omnifyjp/omnify-darwin-x64": "3.13.0",
41
- "@omnifyjp/omnify-linux-x64": "3.13.0",
42
- "@omnifyjp/omnify-linux-arm64": "3.13.0",
43
- "@omnifyjp/omnify-win32-x64": "3.13.0"
39
+ "@omnifyjp/omnify-darwin-arm64": "3.13.1",
40
+ "@omnifyjp/omnify-darwin-x64": "3.13.1",
41
+ "@omnifyjp/omnify-linux-x64": "3.13.1",
42
+ "@omnifyjp/omnify-linux-arm64": "3.13.1",
43
+ "@omnifyjp/omnify-win32-x64": "3.13.1"
44
44
  }
45
45
  }
@@ -38,6 +38,15 @@ export interface FormField {
38
38
  nullable: boolean;
39
39
  /** Default value for `empty<Model>CreateForm()`, as a JS literal string. */
40
40
  defaultLiteral: string;
41
+ /**
42
+ * When true, the field is intentionally omitted from the
43
+ * `empty<Model>CreateForm()` factory body. Used for optional enum
44
+ * reference fields where no sensible default exists — the interface
45
+ * marks the field `?:` so omitting the key is valid, and this is
46
+ * safer than emitting `undefined` under `exactOptionalPropertyTypes`.
47
+ * Issue omnify-jp/omnify-go#56.
48
+ */
49
+ omitFromEmpty?: boolean;
41
50
  }
42
51
  /** Resolved per-schema form metadata. */
43
52
  export interface SchemaFormShape {
@@ -22,6 +22,7 @@
22
22
  */
23
23
  import { toSnakeCase } from './interface-generator.js';
24
24
  import { classifyFieldType } from './metadata-generator.js';
25
+ import { toEnumMemberName } from './enum-generator.js';
25
26
  // ---------------------------------------------------------------------------
26
27
  // Build the IR
27
28
  // ---------------------------------------------------------------------------
@@ -71,26 +72,86 @@ function tsTypeForField(metaType, prop) {
71
72
  return 'string';
72
73
  }
73
74
  }
74
- function defaultLiteralForField(metaType, prop) {
75
- // Honor an explicit YAML default when present.
75
+ /**
76
+ * Resolve an enum's value list either from a plugin enum bank
77
+ * (customTypes.enums) or from a schema-defined enum (`kind: enum` schemas).
78
+ * Returns undefined if the enum reference can't be resolved at codegen time.
79
+ */
80
+ function resolveEnumValueList(prop, allSchemas, pluginEnums) {
81
+ if (typeof prop.enum === 'string') {
82
+ // Plugin enum first (customTypes.enums), then schema enum.
83
+ const fromPlugin = pluginEnums[prop.enum];
84
+ if (fromPlugin)
85
+ return fromPlugin;
86
+ const schemaEnum = allSchemas[prop.enum];
87
+ if (schemaEnum?.values && schemaEnum.values.length > 0) {
88
+ return schemaEnum.values.map((v) => v.value);
89
+ }
90
+ return undefined;
91
+ }
92
+ if (Array.isArray(prop.enum)) {
93
+ return prop.enum.map((v) => typeof v === 'string' ? v : v.value);
94
+ }
95
+ return undefined;
96
+ }
97
+ function defaultLiteralForField(metaType, prop, required, allSchemas, pluginEnums) {
98
+ // -----------------------------------------------------------------
99
+ // Enum fields — reference the enum member, not a string literal.
100
+ //
101
+ // `status: 'free'` is not assignable to `TableStatus` under strict
102
+ // TypeScript because enum types are nominal. We must emit
103
+ // `TableStatus.Free` instead. For inline enums (array of strings) the
104
+ // TS type is a literal union, so string literals are fine — that case
105
+ // falls through to the JSON.stringify path at the bottom.
106
+ //
107
+ // Issue omnify-jp/omnify-go#56.
108
+ // -----------------------------------------------------------------
109
+ if (metaType === 'enum' && typeof prop.enum === 'string') {
110
+ const enumName = prop.enum;
111
+ // Optional / nullable enum refs: omit from the factory body entirely.
112
+ // The form-state interface already marks the field `?:`, so skipping
113
+ // the key is a valid object literal under strict TS regardless of
114
+ // `exactOptionalPropertyTypes`.
115
+ if (!required) {
116
+ return { literal: '', omit: true };
117
+ }
118
+ // Pick the default value: honor YAML `default` first, else first
119
+ // declared enum member.
120
+ let rawValue;
121
+ if (prop.default !== undefined && prop.default !== null) {
122
+ rawValue = String(prop.default);
123
+ }
124
+ else {
125
+ const values = resolveEnumValueList(prop, allSchemas, pluginEnums);
126
+ if (values && values.length > 0) {
127
+ rawValue = values[0];
128
+ }
129
+ }
130
+ if (rawValue === undefined) {
131
+ // Can't resolve the enum values at codegen time (unlikely, but
132
+ // keeps the generator total). Fall back to omitting — the dev
133
+ // will get a TS error at form init and can fix it manually.
134
+ return { literal: '', omit: true };
135
+ }
136
+ return { literal: `${enumName}.${toEnumMemberName(rawValue)}` };
137
+ }
138
+ // Honor an explicit YAML default when present (non-enum path).
76
139
  if (prop.default !== undefined && prop.default !== null) {
77
- return JSON.stringify(prop.default);
140
+ return { literal: JSON.stringify(prop.default) };
78
141
  }
79
- // For enums with no default, prefer the first value so the form has a
80
- // valid selection on mount.
142
+ // Inline enum (array form) the form-state tsType is a literal union,
143
+ // so a string literal is directly assignable. Use the first value.
81
144
  if (metaType === 'enum') {
82
145
  if (Array.isArray(prop.enum) && prop.enum.length > 0) {
83
146
  const first = prop.enum[0];
84
147
  const val = typeof first === 'string' ? first : first.value;
85
- return JSON.stringify(val);
148
+ return { literal: JSON.stringify(val) };
86
149
  }
87
- // Enum reference: we don't have the value list here; emit empty
88
- // string and let the dev select on first interaction.
89
- return "''";
150
+ return { literal: "''" };
90
151
  }
91
152
  if (metaType === 'boolean')
92
- return 'false';
93
- return PRIMITIVE_TS_DEFAULTS[metaType];
153
+ return { literal: 'false' };
154
+ return { literal: PRIMITIVE_TS_DEFAULTS[metaType] };
94
155
  }
95
156
  /** Build the form shape IR from a schema. */
96
157
  export function buildFormShape(schema, allSchemas, options) {
@@ -129,6 +190,7 @@ export function buildFormShape(schema, allSchemas, options) {
129
190
  : prop.rules?.required === true
130
191
  ? true
131
192
  : !(prop.nullable ?? false);
193
+ const { literal, omit } = defaultLiteralForField(metaType, prop, required, allSchemas, options.customTypes.enums);
132
194
  fields.push({
133
195
  fieldName,
134
196
  tsType: tsTypeForField(metaType, prop),
@@ -136,7 +198,8 @@ export function buildFormShape(schema, allSchemas, options) {
136
198
  translatable: prop.translatable === true,
137
199
  required,
138
200
  nullable: prop.nullable ?? false,
139
- defaultLiteral: defaultLiteralForField(metaType, prop),
201
+ defaultLiteral: literal,
202
+ omitFromEmpty: omit,
140
203
  });
141
204
  }
142
205
  return { modelName: schema.name, fields };
@@ -184,6 +247,11 @@ export function formatPayloadBuilderSection(shape, defaultLocale) {
184
247
  parts.push(`export function empty${modelName}CreateForm(): ${modelName}CreateFormState {\n`);
185
248
  parts.push(` return {\n`);
186
249
  for (const f of fields) {
250
+ // Skip fields marked for omission (e.g. optional enum references
251
+ // with no sensible default — the interface marks them `?:`).
252
+ // Issue omnify-jp/omnify-go#56.
253
+ if (f.omitFromEmpty)
254
+ continue;
187
255
  if (f.translatable) {
188
256
  parts.push(` ${f.fieldName}: emptyLocaleMap(),\n`);
189
257
  }