@omnifyjp/omnify 0.1.0 → 0.1.2

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": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Schema-driven code generation for Laravel, TypeScript, and SQL",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -8,7 +8,8 @@
8
8
  "url": "https://github.com/omnifyjp/omnify-go"
9
9
  },
10
10
  "bin": {
11
- "omnify": "bin/omnify"
11
+ "omnify": "bin/omnify",
12
+ "omnify-ts": "ts-dist/cli.js"
12
13
  },
13
14
  "types": "types/index.d.ts",
14
15
  "exports": {
@@ -21,7 +22,12 @@
21
22
  "./omnify-schema.json": "./omnify-schema.json",
22
23
  "./omnify-config-schema.json": "./omnify-config-schema.json"
23
24
  },
24
- "files": ["bin/", "types/", "omnify-schema.json", "omnify-config-schema.json", "README.md"],
25
+ "files": ["bin/", "ts-dist/", "types/", "omnify-schema.json", "omnify-config-schema.json", "README.md"],
26
+ "dependencies": {
27
+ "commander": "^13.0.0",
28
+ "yaml": "^2.7.0",
29
+ "zod": "^3.24.0"
30
+ },
25
31
  "optionalDependencies": {
26
32
  "@omnifyjp/omnify-darwin-arm64": "0.1.0",
27
33
  "@omnifyjp/omnify-darwin-x64": "0.1.0",
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @omnify/ts CLI
4
+ *
5
+ * Reads omnify.yaml to resolve schemas.json input and TypeScript output paths.
6
+ * Falls back to explicit --input / --output flags.
7
+ *
8
+ * Usage:
9
+ * omnify-ts # reads omnify.yaml in cwd
10
+ * omnify-ts --config path/to/omnify.yaml
11
+ * omnify-ts --input schemas.json --output ./types # explicit override
12
+ */
13
+ export {};
package/ts-dist/cli.js ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @omnify/ts CLI
4
+ *
5
+ * Reads omnify.yaml to resolve schemas.json input and TypeScript output paths.
6
+ * Falls back to explicit --input / --output flags.
7
+ *
8
+ * Usage:
9
+ * omnify-ts # reads omnify.yaml in cwd
10
+ * omnify-ts --config path/to/omnify.yaml
11
+ * omnify-ts --input schemas.json --output ./types # explicit override
12
+ */
13
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
14
+ import { resolve, dirname, join } from 'node:path';
15
+ import { Command } from 'commander';
16
+ import { parse as parseYaml } from 'yaml';
17
+ import { generateTypeScript } from './generator.js';
18
+ function resolveFromConfig(configPath) {
19
+ const raw = readFileSync(configPath, 'utf-8');
20
+ const config = parseYaml(raw);
21
+ const configDir = dirname(configPath);
22
+ // Find default connection
23
+ const defaultName = config.default ?? 'default';
24
+ const connections = config.connections ?? {};
25
+ const conn = connections[defaultName];
26
+ if (!conn) {
27
+ throw new Error(`Connection "${defaultName}" not found in ${configPath}`);
28
+ }
29
+ // Resolve schemas.json input path
30
+ const schemasPath = conn.output?.laravel?.schemasPath;
31
+ if (!schemasPath) {
32
+ throw new Error(`No output.laravel.schemasPath found for connection "${defaultName}" in ${configPath}.\n` +
33
+ `Either add schemasPath to your config, or use --input explicitly.`);
34
+ }
35
+ // Resolve typescript output path
36
+ const tsPath = conn.output?.typescript?.path;
37
+ if (!tsPath) {
38
+ throw new Error(`No output.typescript.path found for connection "${defaultName}" in ${configPath}.\n` +
39
+ `Add this to your omnify.yaml:\n\n` +
40
+ ` connections:\n` +
41
+ ` ${defaultName}:\n` +
42
+ ` output:\n` +
43
+ ` typescript:\n` +
44
+ ` path: resources/js/types/models\n\n` +
45
+ `Or use --output explicitly.`);
46
+ }
47
+ return {
48
+ input: resolve(configDir, schemasPath),
49
+ output: resolve(configDir, tsPath),
50
+ };
51
+ }
52
+ // ============================================================================
53
+ // CLI
54
+ // ============================================================================
55
+ const program = new Command();
56
+ program
57
+ .name('omnify-ts')
58
+ .description('Generate TypeScript types from Omnify schemas.json')
59
+ .option('-c, --config <path>', 'Path to omnify.yaml (default: ./omnify.yaml)')
60
+ .option('-i, --input <path>', 'Path to schemas.json (overrides config)')
61
+ .option('-o, --output <path>', 'Output directory (overrides config)')
62
+ .option('--force', 'Overwrite user-editable model files', false)
63
+ .action((opts) => {
64
+ let inputPath;
65
+ let outputDir;
66
+ if (opts.input && opts.output) {
67
+ // Explicit flags — skip config
68
+ inputPath = resolve(opts.input);
69
+ outputDir = resolve(opts.output);
70
+ }
71
+ else {
72
+ // Read from omnify.yaml
73
+ const configPath = resolve(opts.config ?? 'omnify.yaml');
74
+ if (!existsSync(configPath)) {
75
+ console.error(`Error: Config not found: ${configPath}\n` +
76
+ `Run from a directory with omnify.yaml, or use --config / --input + --output.`);
77
+ process.exit(1);
78
+ }
79
+ const resolved = resolveFromConfig(configPath);
80
+ inputPath = opts.input ? resolve(opts.input) : resolved.input;
81
+ outputDir = opts.output ? resolve(opts.output) : resolved.output;
82
+ }
83
+ // Read schemas.json
84
+ if (!existsSync(inputPath)) {
85
+ console.error(`Error: schemas.json not found: ${inputPath}\nRun "omnify generate" first.`);
86
+ process.exit(1);
87
+ }
88
+ const raw = readFileSync(inputPath, 'utf-8');
89
+ const input = JSON.parse(raw);
90
+ console.log(`Reading schemas from ${inputPath}`);
91
+ console.log(`Output directory: ${outputDir}`);
92
+ // Generate files
93
+ const files = generateTypeScript(input);
94
+ // Ensure output directories exist
95
+ mkdirSync(join(outputDir, 'base'), { recursive: true });
96
+ mkdirSync(join(outputDir, 'enum'), { recursive: true });
97
+ // Write files
98
+ let created = 0;
99
+ let overwritten = 0;
100
+ let skipped = 0;
101
+ for (const file of files) {
102
+ // Route files to correct subdirectory
103
+ let filePath;
104
+ if (file.category === 'enum' || file.category === 'plugin-enum') {
105
+ filePath = join(outputDir, 'enum', file.filePath);
106
+ }
107
+ else {
108
+ filePath = join(outputDir, file.filePath);
109
+ }
110
+ // Ensure parent directory exists
111
+ mkdirSync(dirname(filePath), { recursive: true });
112
+ // Skip user-editable files if they already exist (unless --force)
113
+ if (!file.overwrite && existsSync(filePath) && !opts.force) {
114
+ skipped++;
115
+ continue;
116
+ }
117
+ writeFileSync(filePath, file.content, 'utf-8');
118
+ if (file.overwrite || !existsSync(filePath)) {
119
+ overwritten++;
120
+ }
121
+ else {
122
+ created++;
123
+ }
124
+ }
125
+ console.log(`\nGeneration complete:`);
126
+ console.log(` ${overwritten} files written (auto-generated)`);
127
+ if (created > 0)
128
+ console.log(` ${created} files created (user-editable)`);
129
+ if (skipped > 0)
130
+ console.log(` ${skipped} files skipped (already exist)`);
131
+ console.log(`\nTotal: ${files.length} files`);
132
+ });
133
+ program.parse();
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @omnify/ts — TypeScript Enum Generator
3
+ *
4
+ * Generates TypeScript enums with helper methods from schema enum definitions,
5
+ * plugin enums (customTypes.enums), and inline property enums.
6
+ */
7
+ import type { SchemaDefinition, TSEnum, TSTypeAlias, GeneratorOptions } from './types.js';
8
+ /** Convert a string to PascalCase. */
9
+ export declare function toPascalCase(value: string): string;
10
+ /** Convert enum value to valid TypeScript enum member name. */
11
+ export declare function toEnumMemberName(value: string): string;
12
+ /** Generate TSEnum from a schema enum definition. */
13
+ export declare function schemaToEnum(schema: SchemaDefinition, options: GeneratorOptions): TSEnum | null;
14
+ /** Generate enums from all schema enums. */
15
+ export declare function generateEnums(schemas: Record<string, SchemaDefinition>, options: GeneratorOptions): TSEnum[];
16
+ /** Generate enums from plugin enums (customTypes.enums in schemas.json). */
17
+ export declare function generatePluginEnums(pluginEnums: Record<string, string[]>, _options: GeneratorOptions): TSEnum[];
18
+ /** Format a TypeScript enum with helpers (Values array, type guard, label getter). */
19
+ export declare function formatEnum(enumDef: TSEnum): string;
20
+ /** Format a TypeScript type alias with helpers. */
21
+ export declare function formatTypeAlias(alias: TSTypeAlias): string;
22
+ /** Result of extracting inline enums. */
23
+ export interface ExtractedInlineEnum {
24
+ typeAlias?: TSTypeAlias;
25
+ enum?: TSEnum;
26
+ }
27
+ /** Extract inline enums from Enum/Select properties. */
28
+ export declare function extractInlineEnums(schemas: Record<string, SchemaDefinition>, options: GeneratorOptions): ExtractedInlineEnum[];
@@ -0,0 +1,253 @@
1
+ /**
2
+ * @omnify/ts — TypeScript Enum Generator
3
+ *
4
+ * Generates TypeScript enums with helper methods from schema enum definitions,
5
+ * plugin enums (customTypes.enums), and inline property enums.
6
+ */
7
+ /** Convert a string to PascalCase. */
8
+ export function toPascalCase(value) {
9
+ const normalized = value.replace(/([a-z])([A-Z])/g, '$1_$2');
10
+ return normalized
11
+ .split(/[-_\s]+/)
12
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
13
+ .join('');
14
+ }
15
+ /** Convert enum value to valid TypeScript enum member name. */
16
+ export function toEnumMemberName(value) {
17
+ let result = value
18
+ .split(/[-_\s]+/)
19
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
20
+ .join('')
21
+ .replace(/[^a-zA-Z0-9]/g, '');
22
+ if (/^\d/.test(result)) {
23
+ result = '_' + result;
24
+ }
25
+ return result;
26
+ }
27
+ /** Parse an enum value from schema (string or object with value/label). */
28
+ function parseEnumValue(value, options) {
29
+ if (typeof value === 'string') {
30
+ return { name: toEnumMemberName(value), value };
31
+ }
32
+ let label;
33
+ if (value.label !== undefined) {
34
+ if (typeof value.label === 'object') {
35
+ // Multi-locale: keep all locales
36
+ label = value.label;
37
+ }
38
+ else {
39
+ label = value.label;
40
+ }
41
+ }
42
+ return {
43
+ name: toEnumMemberName(value.value),
44
+ value: value.value,
45
+ label,
46
+ extra: value.extra,
47
+ };
48
+ }
49
+ /** Generate TSEnum from a schema enum definition. */
50
+ export function schemaToEnum(schema, options) {
51
+ if (schema.kind !== 'enum' || !schema.values)
52
+ return null;
53
+ const values = schema.values.map(v => parseEnumValue(v, options));
54
+ const displayName = resolveString(schema.displayName, options);
55
+ return {
56
+ name: schema.name,
57
+ values,
58
+ comment: displayName ?? schema.name,
59
+ };
60
+ }
61
+ /** Generate enums from all schema enums. */
62
+ export function generateEnums(schemas, options) {
63
+ const enums = [];
64
+ for (const schema of Object.values(schemas)) {
65
+ if (schema.kind === 'enum') {
66
+ const enumDef = schemaToEnum(schema, options);
67
+ if (enumDef)
68
+ enums.push(enumDef);
69
+ }
70
+ }
71
+ return enums;
72
+ }
73
+ /** Generate enums from plugin enums (customTypes.enums in schemas.json). */
74
+ export function generatePluginEnums(pluginEnums, _options) {
75
+ const enums = [];
76
+ for (const [name, values] of Object.entries(pluginEnums)) {
77
+ enums.push({
78
+ name,
79
+ values: values.map(v => ({
80
+ name: toEnumMemberName(v),
81
+ value: v,
82
+ })),
83
+ comment: name,
84
+ });
85
+ }
86
+ return enums;
87
+ }
88
+ /** Check if label is multi-locale. */
89
+ function isMultiLocaleLabel(label) {
90
+ return label !== undefined && typeof label === 'object';
91
+ }
92
+ /** Lowercase first character. */
93
+ function lowerFirst(str) {
94
+ return str.charAt(0).toLowerCase() + str.slice(1);
95
+ }
96
+ /** Escape single quotes in strings. */
97
+ function escapeString(str) {
98
+ return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
99
+ }
100
+ /** Format a TypeScript enum with helpers (Values array, type guard, label getter). */
101
+ export function formatEnum(enumDef) {
102
+ const { name, values, comment } = enumDef;
103
+ const parts = [];
104
+ if (comment) {
105
+ parts.push(`/**\n * ${comment}\n */\n`);
106
+ }
107
+ // Enum definition
108
+ const enumValues = values.map(v => ` ${v.name} = '${v.value}',`).join('\n');
109
+ parts.push(`export enum ${name} {\n${enumValues}\n}\n\n`);
110
+ // Values array
111
+ parts.push(`/** All ${name} values */\n`);
112
+ parts.push(`export const ${name}Values = Object.values(${name}) as ${name}[];\n\n`);
113
+ // Type guard
114
+ parts.push(`/** Type guard for ${name} */\n`);
115
+ parts.push(`export function is${name}(value: unknown): value is ${name} {\n`);
116
+ parts.push(` return ${name}Values.includes(value as ${name});\n`);
117
+ parts.push(`}\n\n`);
118
+ // Labels
119
+ const hasLabels = values.some(v => v.label !== undefined);
120
+ const hasMultiLocale = values.some(v => isMultiLocaleLabel(v.label));
121
+ if (hasLabels) {
122
+ if (hasMultiLocale) {
123
+ const labelEntries = values
124
+ .filter(v => v.label !== undefined)
125
+ .map(v => {
126
+ if (isMultiLocaleLabel(v.label)) {
127
+ const locales = Object.entries(v.label)
128
+ .map(([locale, text]) => `'${locale}': '${escapeString(text)}'`)
129
+ .join(', ');
130
+ return ` [${name}.${v.name}]: { ${locales} },`;
131
+ }
132
+ return ` [${name}.${v.name}]: { default: '${escapeString(String(v.label))}' },`;
133
+ })
134
+ .join('\n');
135
+ parts.push(`const ${lowerFirst(name)}Labels: Partial<Record<${name}, Record<string, string>>> = {\n${labelEntries}\n};\n\n`);
136
+ parts.push(`/** Get label for ${name} value with locale support */\n`);
137
+ parts.push(`export function get${name}Label(value: ${name}, locale?: string): string {\n`);
138
+ parts.push(` const labels = ${lowerFirst(name)}Labels[value];\n`);
139
+ parts.push(` if (!labels) return value;\n`);
140
+ parts.push(` if (locale && labels[locale]) return labels[locale];\n`);
141
+ parts.push(` return labels['ja'] ?? labels['en'] ?? Object.values(labels)[0] ?? value;\n`);
142
+ parts.push(`}\n\n`);
143
+ }
144
+ else {
145
+ const labelEntries = values
146
+ .filter(v => v.label !== undefined)
147
+ .map(v => ` [${name}.${v.name}]: '${escapeString(String(v.label))}',`)
148
+ .join('\n');
149
+ parts.push(`const ${lowerFirst(name)}Labels: Partial<Record<${name}, string>> = {\n${labelEntries}\n};\n\n`);
150
+ parts.push(`/** Get label for ${name} value */\n`);
151
+ parts.push(`export function get${name}Label(value: ${name}): string {\n`);
152
+ parts.push(` return ${lowerFirst(name)}Labels[value] ?? value;\n`);
153
+ parts.push(`}\n\n`);
154
+ }
155
+ }
156
+ else {
157
+ parts.push(`/** Get label for ${name} value */\n`);
158
+ parts.push(`export function get${name}Label(value: ${name}): string {\n`);
159
+ parts.push(` return value;\n`);
160
+ parts.push(`}\n\n`);
161
+ }
162
+ // Extra
163
+ const hasExtra = values.some(v => v.extra !== undefined);
164
+ if (hasExtra) {
165
+ const extraEntries = values
166
+ .filter(v => v.extra !== undefined)
167
+ .map(v => ` [${name}.${v.name}]: ${JSON.stringify(v.extra)},`)
168
+ .join('\n');
169
+ parts.push(`const ${lowerFirst(name)}Extra: Partial<Record<${name}, Record<string, unknown>>> = {\n${extraEntries}\n};\n\n`);
170
+ parts.push(`/** Get extra metadata for ${name} value */\n`);
171
+ parts.push(`export function get${name}Extra(value: ${name}): Record<string, unknown> | undefined {\n`);
172
+ parts.push(` return ${lowerFirst(name)}Extra[value];\n`);
173
+ parts.push(`}`);
174
+ }
175
+ else {
176
+ parts.push(`/** Get extra metadata for ${name} value */\n`);
177
+ parts.push(`export function get${name}Extra(_value: ${name}): Record<string, unknown> | undefined {\n`);
178
+ parts.push(` return undefined;\n`);
179
+ parts.push(`}`);
180
+ }
181
+ return parts.join('');
182
+ }
183
+ /** Format a TypeScript type alias with helpers. */
184
+ export function formatTypeAlias(alias) {
185
+ const { name, type, comment } = alias;
186
+ const parts = [];
187
+ if (comment) {
188
+ parts.push(`/**\n * ${comment}\n */\n`);
189
+ }
190
+ parts.push(`export type ${name} = ${type};\n\n`);
191
+ const values = type.split(' | ').map(v => v.trim());
192
+ parts.push(`/** All ${name} values */\n`);
193
+ parts.push(`export const ${name}Values: ${name}[] = [${values.join(', ')}];\n\n`);
194
+ parts.push(`/** Type guard for ${name} */\n`);
195
+ parts.push(`export function is${name}(value: unknown): value is ${name} {\n`);
196
+ parts.push(` return ${name}Values.includes(value as ${name});\n`);
197
+ parts.push(`}\n\n`);
198
+ parts.push(`/** Get label for ${name} value */\n`);
199
+ parts.push(`export function get${name}Label(value: ${name}): string {\n`);
200
+ parts.push(` return value;\n`);
201
+ parts.push(`}\n\n`);
202
+ parts.push(`/** Get extra metadata for ${name} value */\n`);
203
+ parts.push(`export function get${name}Extra(_value: ${name}): Record<string, unknown> | undefined {\n`);
204
+ parts.push(` return undefined;\n`);
205
+ parts.push(`}`);
206
+ return parts.join('');
207
+ }
208
+ /** Extract inline enums from Enum/Select properties. */
209
+ export function extractInlineEnums(schemas, options) {
210
+ const results = [];
211
+ for (const schema of Object.values(schemas)) {
212
+ if (schema.kind === 'enum' || !schema.properties)
213
+ continue;
214
+ for (const [propName, property] of Object.entries(schema.properties)) {
215
+ if (property.type === 'Enum' && Array.isArray(property.enum) && property.enum.length > 0) {
216
+ const typeName = `${schema.name}${toPascalCase(propName)}`;
217
+ const displayName = resolveString(schema.displayName, options);
218
+ // Check if values have labels (i.e., are objects not strings)
219
+ const enumValues = property.enum;
220
+ const hasLabels = enumValues.some(v => typeof v !== 'string' && v.label !== undefined);
221
+ if (hasLabels) {
222
+ const values = enumValues.map(v => parseEnumValue(v, options));
223
+ results.push({
224
+ enum: {
225
+ name: typeName,
226
+ values,
227
+ comment: displayName ?? `${schema.name} ${propName} enum`,
228
+ },
229
+ });
230
+ }
231
+ else {
232
+ const values = enumValues.map(v => typeof v === 'string' ? v : v.value);
233
+ results.push({
234
+ typeAlias: {
235
+ name: typeName,
236
+ type: values.map(v => `'${v}'`).join(' | '),
237
+ comment: displayName ?? `${schema.name} ${propName} enum`,
238
+ },
239
+ });
240
+ }
241
+ }
242
+ }
243
+ }
244
+ return results;
245
+ }
246
+ /** Resolve a LocalizedString to a single string. */
247
+ function resolveString(value, options) {
248
+ if (value === undefined)
249
+ return undefined;
250
+ if (typeof value === 'string')
251
+ return value;
252
+ return value[options.defaultLocale] ?? value[options.fallbackLocale] ?? value['en'] ?? Object.values(value)[0];
253
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @omnify/ts — Main Generator
3
+ *
4
+ * Orchestrates TypeScript code generation from schemas.json.
5
+ * Reads the JSON, builds options, calls sub-generators, and assembles output files.
6
+ *
7
+ * Output structure:
8
+ * base/{SchemaName}.ts — Auto-generated interfaces + Zod + i18n (always overwritten)
9
+ * enum/{EnumName}.ts — Auto-generated enums (always overwritten)
10
+ * common.ts — Shared types (DateTimeString, etc.)
11
+ * i18n.ts — Validation messages + locale helpers
12
+ * {SchemaName}.ts — User-editable models extending base (created once, never overwritten)
13
+ * index.ts — Re-exports (always overwritten)
14
+ */
15
+ import type { SchemasJson, TypeScriptFile } from './types.js';
16
+ /**
17
+ * Generate all TypeScript files from schemas.json input.
18
+ */
19
+ export declare function generateTypeScript(input: SchemasJson): TypeScriptFile[];