@omnifyjp/omnify 3.12.4 → 3.13.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/package.json +6 -6
- package/ts-dist/cli.d.ts +13 -0
- package/ts-dist/cli.js +83 -33
- package/ts-dist/enum-generator.js +20 -0
- package/ts-dist/generator.js +101 -1
- package/ts-dist/input-resolver.d.ts +84 -0
- package/ts-dist/input-resolver.js +246 -0
- package/ts-dist/metadata-generator.d.ts +76 -0
- package/ts-dist/metadata-generator.js +329 -0
- package/ts-dist/payload-builder-generator.d.ts +50 -0
- package/ts-dist/payload-builder-generator.js +271 -0
- package/ts-dist/zod-generator.d.ts +1 -1
- package/ts-dist/zod-generator.js +68 -1
- package/types/config.d.ts +14 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @omnify/ts — Payload Builder Generator (issue omnify-jp/omnify-go#55)
|
|
3
|
+
*
|
|
4
|
+
* Emits per-model form-state interfaces and payload-builder helpers
|
|
5
|
+
* alongside the existing zod schemas. Translatable fields are typed as
|
|
6
|
+
* `Record<Locale, string>` in the form state (matching the design
|
|
7
|
+
* system's `<Input translatable />` component shape) and the builder
|
|
8
|
+
* collapses them into the nested locale-keyed payload that the create /
|
|
9
|
+
* update zod schemas accept (see issue #53).
|
|
10
|
+
*
|
|
11
|
+
* Output (added to base/<Model>.ts):
|
|
12
|
+
*
|
|
13
|
+
* export interface <Model>CreateFormState { ... }
|
|
14
|
+
* export interface <Model>UpdateFormState { ... }
|
|
15
|
+
* export function empty<Model>CreateForm() { ... }
|
|
16
|
+
* export function build<Model>CreatePayload(form) { ... }
|
|
17
|
+
* export function build<Model>UpdatePayload(form) { ... }
|
|
18
|
+
*
|
|
19
|
+
* The shared `buildI18nPayload` utility is emitted exactly once at the
|
|
20
|
+
* codegen output root in `payload-helpers.ts` so per-model files can
|
|
21
|
+
* import it.
|
|
22
|
+
*/
|
|
23
|
+
import type { GeneratorOptions, SchemaDefinition } from './types.js';
|
|
24
|
+
import { type MetadataFieldType } from './metadata-generator.js';
|
|
25
|
+
/** A single field as seen by the form-state / payload builder. */
|
|
26
|
+
export interface FormField {
|
|
27
|
+
/** Field key as it appears on the wire (snake_case, with `_id` suffix on FKs). */
|
|
28
|
+
fieldName: string;
|
|
29
|
+
/** TypeScript primitive used in the form-state interface. */
|
|
30
|
+
tsType: string;
|
|
31
|
+
/** Coarse type taxonomy from the metadata generator. */
|
|
32
|
+
metaType: MetadataFieldType;
|
|
33
|
+
/** Whether the YAML declared the field translatable. */
|
|
34
|
+
translatable: boolean;
|
|
35
|
+
/** Whether the field is required by the create schema. */
|
|
36
|
+
required: boolean;
|
|
37
|
+
/** Whether the YAML declared the field nullable. */
|
|
38
|
+
nullable: boolean;
|
|
39
|
+
/** Default value for `empty<Model>CreateForm()`, as a JS literal string. */
|
|
40
|
+
defaultLiteral: string;
|
|
41
|
+
}
|
|
42
|
+
/** Resolved per-schema form metadata. */
|
|
43
|
+
export interface SchemaFormShape {
|
|
44
|
+
modelName: string;
|
|
45
|
+
fields: FormField[];
|
|
46
|
+
}
|
|
47
|
+
/** Build the form shape IR from a schema. */
|
|
48
|
+
export declare function buildFormShape(schema: SchemaDefinition, allSchemas: Record<string, SchemaDefinition>, options: GeneratorOptions): SchemaFormShape;
|
|
49
|
+
/** Render the full payload-builder section for a schema. */
|
|
50
|
+
export declare function formatPayloadBuilderSection(shape: SchemaFormShape, defaultLocale: string): string;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @omnify/ts — Payload Builder Generator (issue omnify-jp/omnify-go#55)
|
|
3
|
+
*
|
|
4
|
+
* Emits per-model form-state interfaces and payload-builder helpers
|
|
5
|
+
* alongside the existing zod schemas. Translatable fields are typed as
|
|
6
|
+
* `Record<Locale, string>` in the form state (matching the design
|
|
7
|
+
* system's `<Input translatable />` component shape) and the builder
|
|
8
|
+
* collapses them into the nested locale-keyed payload that the create /
|
|
9
|
+
* update zod schemas accept (see issue #53).
|
|
10
|
+
*
|
|
11
|
+
* Output (added to base/<Model>.ts):
|
|
12
|
+
*
|
|
13
|
+
* export interface <Model>CreateFormState { ... }
|
|
14
|
+
* export interface <Model>UpdateFormState { ... }
|
|
15
|
+
* export function empty<Model>CreateForm() { ... }
|
|
16
|
+
* export function build<Model>CreatePayload(form) { ... }
|
|
17
|
+
* export function build<Model>UpdatePayload(form) { ... }
|
|
18
|
+
*
|
|
19
|
+
* The shared `buildI18nPayload` utility is emitted exactly once at the
|
|
20
|
+
* codegen output root in `payload-helpers.ts` so per-model files can
|
|
21
|
+
* import it.
|
|
22
|
+
*/
|
|
23
|
+
import { toSnakeCase } from './interface-generator.js';
|
|
24
|
+
import { classifyFieldType } from './metadata-generator.js';
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Build the IR
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
const PRIMITIVE_TS_DEFAULTS = {
|
|
29
|
+
string: "''",
|
|
30
|
+
text: "''",
|
|
31
|
+
number: '0',
|
|
32
|
+
boolean: 'false',
|
|
33
|
+
date: "''",
|
|
34
|
+
datetime: "''",
|
|
35
|
+
time: "''",
|
|
36
|
+
json: 'null',
|
|
37
|
+
enum: "''",
|
|
38
|
+
uuid: "''",
|
|
39
|
+
file: 'null',
|
|
40
|
+
compound: "''",
|
|
41
|
+
};
|
|
42
|
+
function tsTypeForField(metaType, prop) {
|
|
43
|
+
switch (metaType) {
|
|
44
|
+
case 'string':
|
|
45
|
+
case 'text':
|
|
46
|
+
case 'date':
|
|
47
|
+
case 'datetime':
|
|
48
|
+
case 'time':
|
|
49
|
+
case 'uuid':
|
|
50
|
+
return 'string';
|
|
51
|
+
case 'number':
|
|
52
|
+
return 'number';
|
|
53
|
+
case 'boolean':
|
|
54
|
+
return 'boolean';
|
|
55
|
+
case 'json':
|
|
56
|
+
return 'unknown';
|
|
57
|
+
case 'file':
|
|
58
|
+
return 'unknown';
|
|
59
|
+
case 'enum':
|
|
60
|
+
// Use the enum reference name when available so downstream form
|
|
61
|
+
// components get type narrowing on the union.
|
|
62
|
+
if (typeof prop.enum === 'string')
|
|
63
|
+
return prop.enum;
|
|
64
|
+
if (Array.isArray(prop.enum)) {
|
|
65
|
+
return prop.enum
|
|
66
|
+
.map((v) => `'${typeof v === 'string' ? v : v.value}'`)
|
|
67
|
+
.join(' | ');
|
|
68
|
+
}
|
|
69
|
+
return 'string';
|
|
70
|
+
default:
|
|
71
|
+
return 'string';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function defaultLiteralForField(metaType, prop) {
|
|
75
|
+
// Honor an explicit YAML default when present.
|
|
76
|
+
if (prop.default !== undefined && prop.default !== null) {
|
|
77
|
+
return JSON.stringify(prop.default);
|
|
78
|
+
}
|
|
79
|
+
// For enums with no default, prefer the first value so the form has a
|
|
80
|
+
// valid selection on mount.
|
|
81
|
+
if (metaType === 'enum') {
|
|
82
|
+
if (Array.isArray(prop.enum) && prop.enum.length > 0) {
|
|
83
|
+
const first = prop.enum[0];
|
|
84
|
+
const val = typeof first === 'string' ? first : first.value;
|
|
85
|
+
return JSON.stringify(val);
|
|
86
|
+
}
|
|
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 "''";
|
|
90
|
+
}
|
|
91
|
+
if (metaType === 'boolean')
|
|
92
|
+
return 'false';
|
|
93
|
+
return PRIMITIVE_TS_DEFAULTS[metaType];
|
|
94
|
+
}
|
|
95
|
+
/** Build the form shape IR from a schema. */
|
|
96
|
+
export function buildFormShape(schema, allSchemas, options) {
|
|
97
|
+
const fields = [];
|
|
98
|
+
if (!schema.properties) {
|
|
99
|
+
return { modelName: schema.name, fields };
|
|
100
|
+
}
|
|
101
|
+
const propNames = schema.propertyOrder ?? Object.keys(schema.properties);
|
|
102
|
+
for (const propName of propNames) {
|
|
103
|
+
const prop = schema.properties[propName];
|
|
104
|
+
if (!prop)
|
|
105
|
+
continue;
|
|
106
|
+
// Skip inverse-side associations — they have no column.
|
|
107
|
+
if (prop.type === 'Association' &&
|
|
108
|
+
(prop.mappedBy || prop.relation === 'OneToMany' ||
|
|
109
|
+
prop.relation === 'ManyToMany' || prop.relation === 'MorphMany' ||
|
|
110
|
+
prop.relation === 'MorphTo' || prop.relation === 'MorphToMany' ||
|
|
111
|
+
prop.relation === 'MorphedByMany')) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// Skip File for the form-state shape — file uploads have a different
|
|
115
|
+
// lifecycle (multipart form, temp tokens) handled outside the JSON
|
|
116
|
+
// payload builder.
|
|
117
|
+
if (prop.type === 'File')
|
|
118
|
+
continue;
|
|
119
|
+
const metaType = classifyFieldType(prop, allSchemas, options.customTypes.simple);
|
|
120
|
+
const isOwningAssoc = prop.type === 'Association' &&
|
|
121
|
+
(prop.relation === 'ManyToOne' || prop.relation === 'OneToOne') &&
|
|
122
|
+
!prop.mappedBy;
|
|
123
|
+
const fieldName = isOwningAssoc
|
|
124
|
+
? `${toSnakeCase(propName)}_id`
|
|
125
|
+
: toSnakeCase(propName);
|
|
126
|
+
// rules.required overrides nullable inference
|
|
127
|
+
const required = prop.rules?.required === false
|
|
128
|
+
? false
|
|
129
|
+
: prop.rules?.required === true
|
|
130
|
+
? true
|
|
131
|
+
: !(prop.nullable ?? false);
|
|
132
|
+
fields.push({
|
|
133
|
+
fieldName,
|
|
134
|
+
tsType: tsTypeForField(metaType, prop),
|
|
135
|
+
metaType,
|
|
136
|
+
translatable: prop.translatable === true,
|
|
137
|
+
required,
|
|
138
|
+
nullable: prop.nullable ?? false,
|
|
139
|
+
defaultLiteral: defaultLiteralForField(metaType, prop),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return { modelName: schema.name, fields };
|
|
143
|
+
}
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Format the form-state interfaces + builder helpers
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
/** Render the full payload-builder section for a schema. */
|
|
148
|
+
export function formatPayloadBuilderSection(shape, defaultLocale) {
|
|
149
|
+
const { modelName, fields } = shape;
|
|
150
|
+
if (fields.length === 0)
|
|
151
|
+
return '';
|
|
152
|
+
const parts = [];
|
|
153
|
+
parts.push(`// ============================================================================\n`);
|
|
154
|
+
parts.push(`// Form State + Payload Builders (issue #55)\n`);
|
|
155
|
+
parts.push(`// ============================================================================\n\n`);
|
|
156
|
+
// ---- Create form state interface ----
|
|
157
|
+
parts.push(`/** Form-state shape for creating a ${modelName}. Translatable fields are\n` +
|
|
158
|
+
` * flat per-locale maps; the builder collapses them into the wire shape. */\n`);
|
|
159
|
+
parts.push(`export interface ${modelName}CreateFormState {\n`);
|
|
160
|
+
for (const f of fields) {
|
|
161
|
+
if (f.translatable) {
|
|
162
|
+
parts.push(` ${f.fieldName}: Record<Locale, string>;\n`);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const optional = !f.required;
|
|
166
|
+
parts.push(` ${f.fieldName}${optional ? '?' : ''}: ${f.tsType}${f.nullable ? ' | null' : ''};\n`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
parts.push(`}\n\n`);
|
|
170
|
+
// ---- Update form state interface ----
|
|
171
|
+
parts.push(`/** Form-state shape for updating a ${modelName}. Every field optional. */\n`);
|
|
172
|
+
parts.push(`export interface ${modelName}UpdateFormState {\n`);
|
|
173
|
+
for (const f of fields) {
|
|
174
|
+
if (f.translatable) {
|
|
175
|
+
parts.push(` ${f.fieldName}?: Record<Locale, string>;\n`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
parts.push(` ${f.fieldName}?: ${f.tsType}${f.nullable ? ' | null' : ''};\n`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
parts.push(`}\n\n`);
|
|
182
|
+
// ---- empty<Model>CreateForm factory ----
|
|
183
|
+
parts.push(`/** Default-state factory for the create form. Pre-fills every locale key. */\n`);
|
|
184
|
+
parts.push(`export function empty${modelName}CreateForm(): ${modelName}CreateFormState {\n`);
|
|
185
|
+
parts.push(` return {\n`);
|
|
186
|
+
for (const f of fields) {
|
|
187
|
+
if (f.translatable) {
|
|
188
|
+
parts.push(` ${f.fieldName}: emptyLocaleMap(),\n`);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
parts.push(` ${f.fieldName}: ${f.defaultLiteral},\n`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
parts.push(` };\n`);
|
|
195
|
+
parts.push(`}\n\n`);
|
|
196
|
+
// ---- buildCreatePayload ----
|
|
197
|
+
parts.push(`/** Convert create form state into the payload that ` +
|
|
198
|
+
`base${modelName}CreateSchema accepts. */\n`);
|
|
199
|
+
parts.push(`export function build${modelName}CreatePayload(form: ${modelName}CreateFormState): Base${modelName}Create {\n`);
|
|
200
|
+
// Field-by-field assignment.
|
|
201
|
+
parts.push(` const payload: Record<string, unknown> = {\n`);
|
|
202
|
+
for (const f of fields) {
|
|
203
|
+
if (f.translatable) {
|
|
204
|
+
// Top-level mirror = default-locale value, trimmed.
|
|
205
|
+
parts.push(` ${f.fieldName}: (form.${f.fieldName}[${JSON.stringify(defaultLocale)}] ?? '').trim(),\n`);
|
|
206
|
+
}
|
|
207
|
+
else if (f.metaType === 'string' || f.metaType === 'text' || f.metaType === 'uuid') {
|
|
208
|
+
if (f.required) {
|
|
209
|
+
parts.push(` ${f.fieldName}: form.${f.fieldName}.trim(),\n`);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// Drop empty optional strings.
|
|
213
|
+
parts.push(` ${f.fieldName}: form.${f.fieldName}?.toString().trim() || ${f.nullable ? 'null' : 'undefined'},\n`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else if (f.metaType === 'date' || f.metaType === 'datetime') {
|
|
217
|
+
// Already a string in form state; pass through.
|
|
218
|
+
parts.push(` ${f.fieldName}: form.${f.fieldName},\n`);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
parts.push(` ${f.fieldName}: form.${f.fieldName},\n`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
parts.push(` };\n\n`);
|
|
225
|
+
// Spread per-locale sub-objects (issue #53 wire shape).
|
|
226
|
+
const translatableFieldNames = fields.filter((f) => f.translatable).map((f) => f.fieldName);
|
|
227
|
+
if (translatableFieldNames.length > 0) {
|
|
228
|
+
parts.push(` // Per-locale sub-objects (matches issue #53 wire shape).\n`);
|
|
229
|
+
parts.push(` const i18n = buildI18nPayload({\n`);
|
|
230
|
+
for (const name of translatableFieldNames) {
|
|
231
|
+
parts.push(` ${name}: form.${name},\n`);
|
|
232
|
+
}
|
|
233
|
+
parts.push(` });\n`);
|
|
234
|
+
parts.push(` Object.assign(payload, i18n);\n\n`);
|
|
235
|
+
}
|
|
236
|
+
parts.push(` return payload as Base${modelName}Create;\n`);
|
|
237
|
+
parts.push(`}\n\n`);
|
|
238
|
+
// ---- buildUpdatePayload ----
|
|
239
|
+
parts.push(`/** Convert update form state into the payload that ` +
|
|
240
|
+
`base${modelName}UpdateSchema accepts. Only fields present on \`form\` are emitted. */\n`);
|
|
241
|
+
parts.push(`export function build${modelName}UpdatePayload(form: ${modelName}UpdateFormState): Base${modelName}Update {\n`);
|
|
242
|
+
parts.push(` const payload: Record<string, unknown> = {};\n`);
|
|
243
|
+
for (const f of fields) {
|
|
244
|
+
if (f.translatable) {
|
|
245
|
+
parts.push(` if (form.${f.fieldName} !== undefined) {\n`);
|
|
246
|
+
parts.push(` payload.${f.fieldName} = (form.${f.fieldName}[${JSON.stringify(defaultLocale)}] ?? '').trim();\n`);
|
|
247
|
+
parts.push(` }\n`);
|
|
248
|
+
}
|
|
249
|
+
else if (f.metaType === 'string' || f.metaType === 'text' || f.metaType === 'uuid') {
|
|
250
|
+
parts.push(` if (form.${f.fieldName} !== undefined) {\n`);
|
|
251
|
+
parts.push(` payload.${f.fieldName} = form.${f.fieldName}?.toString().trim() ?? null;\n`);
|
|
252
|
+
parts.push(` }\n`);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
parts.push(` if (form.${f.fieldName} !== undefined) {\n`);
|
|
256
|
+
parts.push(` payload.${f.fieldName} = form.${f.fieldName};\n`);
|
|
257
|
+
parts.push(` }\n`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (translatableFieldNames.length > 0) {
|
|
261
|
+
parts.push(`\n // Per-locale sub-objects only for fields the user touched.\n`);
|
|
262
|
+
parts.push(` const localeInput: Record<string, Record<string, string> | undefined> = {};\n`);
|
|
263
|
+
for (const name of translatableFieldNames) {
|
|
264
|
+
parts.push(` if (form.${name} !== undefined) localeInput.${name} = form.${name};\n`);
|
|
265
|
+
}
|
|
266
|
+
parts.push(` Object.assign(payload, buildI18nPayload(localeInput));\n`);
|
|
267
|
+
}
|
|
268
|
+
parts.push(` return payload as Base${modelName}Update;\n`);
|
|
269
|
+
parts.push(`}\n`);
|
|
270
|
+
return parts.join('');
|
|
271
|
+
}
|
|
@@ -25,7 +25,7 @@ export declare function getExcludedFields(schema: SchemaDefinition): {
|
|
|
25
25
|
export declare function formatZodSchemasSection(schemaName: string, zodSchemas: ZodPropertySchema[], displayNames: SchemaDisplayNames, excludedFields: {
|
|
26
26
|
create: Set<string>;
|
|
27
27
|
update: Set<string>;
|
|
28
|
-
}): string;
|
|
28
|
+
}, schema: SchemaDefinition, options: GeneratorOptions): string;
|
|
29
29
|
/**
|
|
30
30
|
* Format user model file with Zod re-exports.
|
|
31
31
|
*/
|
package/ts-dist/zod-generator.js
CHANGED
|
@@ -409,10 +409,69 @@ export function getExcludedFields(schema) {
|
|
|
409
409
|
}
|
|
410
410
|
return { create: createExclude, update: updateExclude };
|
|
411
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Build the per-locale sub-object lines for a schema's translatable fields.
|
|
414
|
+
*
|
|
415
|
+
* For every property with `translatable: true`, we synthesize a "translation
|
|
416
|
+
* variant" of its zod schema by forcing `nullable: true` and clearing
|
|
417
|
+
* `rules.required`. This means:
|
|
418
|
+
* - `String` translatable fields drop the implicit `.min(1)` (translation
|
|
419
|
+
* rows for non-default locales may legitimately be empty).
|
|
420
|
+
* - `max(N)` / `length(N)` validators are preserved so client and server
|
|
421
|
+
* agree on bounds.
|
|
422
|
+
* - The variant ends in `.optional().nullable()` so missing translations
|
|
423
|
+
* don't break validation.
|
|
424
|
+
*
|
|
425
|
+
* Each locale (from `options.locales`) gets its own sub-object containing
|
|
426
|
+
* only the translatable fields. The sub-object itself is `.optional()` so
|
|
427
|
+
* a payload may omit any locale entirely. Implements omnify-jp/omnify-go#53.
|
|
428
|
+
*
|
|
429
|
+
* Returns the lines (without trailing newlines) ready to splice into the
|
|
430
|
+
* `z.object({ ... })` body of the create schema, or an empty array when
|
|
431
|
+
* the schema has no translatable fields.
|
|
432
|
+
*/
|
|
433
|
+
function buildTranslatableLocaleSchemaLines(schema, options) {
|
|
434
|
+
if (!schema.properties || options.locales.length === 0)
|
|
435
|
+
return [];
|
|
436
|
+
// Collect translatable fields in declaration order so the generated
|
|
437
|
+
// sub-object body matches the rest of the file.
|
|
438
|
+
const translatableProps = [];
|
|
439
|
+
const propNames = schema.propertyOrder ?? Object.keys(schema.properties);
|
|
440
|
+
for (const propName of propNames) {
|
|
441
|
+
const propDef = schema.properties[propName];
|
|
442
|
+
if (!propDef || !propDef.translatable)
|
|
443
|
+
continue;
|
|
444
|
+
// Build the translation variant: nullable, no `required` rule. The
|
|
445
|
+
// existing zod generator handles the rest (max length, regex, etc.).
|
|
446
|
+
const variantDef = {
|
|
447
|
+
...propDef,
|
|
448
|
+
nullable: true,
|
|
449
|
+
rules: propDef.rules ? { ...propDef.rules, required: false } : undefined,
|
|
450
|
+
};
|
|
451
|
+
const variantSchema = getZodSchemaForType(variantDef, propName, options);
|
|
452
|
+
if (!variantSchema)
|
|
453
|
+
continue;
|
|
454
|
+
translatableProps.push({
|
|
455
|
+
fieldName: toSnakeCase(propName),
|
|
456
|
+
schema: variantSchema,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
if (translatableProps.length === 0)
|
|
460
|
+
return [];
|
|
461
|
+
const lines = [];
|
|
462
|
+
for (const locale of options.locales) {
|
|
463
|
+
lines.push(` ${locale}: z.object({`);
|
|
464
|
+
for (const tp of translatableProps) {
|
|
465
|
+
lines.push(` ${tp.fieldName}: ${tp.schema},`);
|
|
466
|
+
}
|
|
467
|
+
lines.push(` }).optional(),`);
|
|
468
|
+
}
|
|
469
|
+
return lines;
|
|
470
|
+
}
|
|
412
471
|
/**
|
|
413
472
|
* Format Zod schemas section for a base file.
|
|
414
473
|
*/
|
|
415
|
-
export function formatZodSchemasSection(schemaName, zodSchemas, displayNames, excludedFields) {
|
|
474
|
+
export function formatZodSchemasSection(schemaName, zodSchemas, displayNames, excludedFields, schema, options) {
|
|
416
475
|
const parts = [];
|
|
417
476
|
const lowerName = schemaName.charAt(0).toLowerCase() + schemaName.slice(1);
|
|
418
477
|
// I18n section
|
|
@@ -451,6 +510,14 @@ export function formatZodSchemasSection(schemaName, zodSchemas, displayNames, ex
|
|
|
451
510
|
for (const prop of createFields) {
|
|
452
511
|
parts.push(` ${prop.fieldName}: base${schemaName}Schemas.${prop.fieldName},\n`);
|
|
453
512
|
}
|
|
513
|
+
// Per-locale sub-objects for translatable fields. The Update schema
|
|
514
|
+
// (which is `.partial()` of Create) inherits these for free, so the
|
|
515
|
+
// partial form correctly types each locale key as optional too.
|
|
516
|
+
// Issue omnify-jp/omnify-go#53.
|
|
517
|
+
const localeLines = buildTranslatableLocaleSchemaLines(schema, options);
|
|
518
|
+
for (const line of localeLines) {
|
|
519
|
+
parts.push(line + '\n');
|
|
520
|
+
}
|
|
454
521
|
parts.push(`});\n\n`);
|
|
455
522
|
// Update Schema
|
|
456
523
|
parts.push(`/** Update schema for ${schemaName} */\n`);
|
package/types/config.d.ts
CHANGED
|
@@ -31,8 +31,10 @@ export interface ConnectionConfig {
|
|
|
31
31
|
export interface CodegenTypeScriptConfig {
|
|
32
32
|
/** Enable TypeScript codegen. Defaults to false. */
|
|
33
33
|
enable: boolean;
|
|
34
|
-
/** Output directory for generated TypeScript model files. */
|
|
35
|
-
modelsPath
|
|
34
|
+
/** Output directory for generated TypeScript model files (legacy alias for `output`). */
|
|
35
|
+
modelsPath?: string;
|
|
36
|
+
/** Output directory for generated TypeScript model files. Preferred over `modelsPath`; symmetrical with the top-level `input` field used by consumer-mode configs. */
|
|
37
|
+
output?: string;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
/** Laravel codegen path configuration for a target (model, request, etc.). */
|
|
@@ -107,6 +109,16 @@ export interface PackageConfig {
|
|
|
107
109
|
|
|
108
110
|
/** Main Omnify configuration (omnify.yaml). */
|
|
109
111
|
export interface OmnifyConfig {
|
|
112
|
+
/**
|
|
113
|
+
* Consumer-mode shortcut: path/URL/npm-spec for an upstream `schemas.json`.
|
|
114
|
+
* When set, omnify-ts skips the connections+migrations indirection and
|
|
115
|
+
* uses this value directly. Accepts a local path (relative to this file),
|
|
116
|
+
* an http(s) URL (cached + pinned via `.omnify/input.lock.json`), or a
|
|
117
|
+
* scoped npm package specifier (e.g. `@famgia/dxs-product-schemas/schemas.json`).
|
|
118
|
+
* Frontend / consumer projects should use this; backend projects use
|
|
119
|
+
* `schemasDir` + `connections` instead.
|
|
120
|
+
*/
|
|
121
|
+
input?: string;
|
|
110
122
|
/** Directory containing schema YAML files. */
|
|
111
123
|
schemasDir?: string;
|
|
112
124
|
/** External schema packages to consume. */
|