katax-cli 0.1.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.
@@ -0,0 +1,160 @@
1
+ import { toPascalCase, toCamelCase } from '../utils/file-utils.js';
2
+ export function generateValidator(config) {
3
+ const { name, fields = [], addAsyncValidators } = config;
4
+ const pascalName = toPascalCase(name);
5
+ const camelName = toCamelCase(name);
6
+ let content = `import { k, kataxInfer } from 'katax-core';\n`;
7
+ content += `import type { ValidationResult } from '../../shared/api.utils.js';\n`;
8
+ if (addAsyncValidators) {
9
+ content += `import type { AsyncValidator } from 'katax-core';\n`;
10
+ content += `// import pool from '../../database/db.config.js'; // Uncomment if using database\n\n`;
11
+ }
12
+ else {
13
+ content += '\n';
14
+ }
15
+ // Generate async validators if needed
16
+ if (addAsyncValidators) {
17
+ content += `// ==================== ASYNC VALIDATORS ====================\n\n`;
18
+ fields.forEach(field => {
19
+ if (field.asyncValidator) {
20
+ content += `/**\n * Check if ${field.name} is unique\n */\n`;
21
+ content += `export const ${field.name}UniqueValidator: AsyncValidator<string> = async (value, path) => {\n`;
22
+ content += ` console.log(\`[ASYNC] Checking ${field.name}: \${value}\`);\n`;
23
+ content += ` \n`;
24
+ content += ` // TODO: Implement database check\n`;
25
+ content += ` // const result = await pool.query(\n`;
26
+ content += ` // 'SELECT id FROM ${field.asyncValidator.table || 'table_name'} WHERE ${field.asyncValidator.column || field.name} = $1',\n`;
27
+ content += ` // [value]\n`;
28
+ content += ` // );\n`;
29
+ content += ` // \n`;
30
+ content += ` // if (result.rows.length > 0) {\n`;
31
+ content += ` // return [{ path, message: "This ${field.name} is already taken" }];\n`;
32
+ content += ` // }\n`;
33
+ content += ` \n`;
34
+ content += ` return [];\n`;
35
+ content += `};\n\n`;
36
+ }
37
+ });
38
+ }
39
+ // Generate schema
40
+ content += `// ==================== SCHEMAS ====================\n\n`;
41
+ content += `/**\n * Schema for ${name} ${config.method} request\n */\n`;
42
+ content += `export const ${camelName}Schema = k.object({\n`;
43
+ fields.forEach((field, index) => {
44
+ const isLast = index === fields.length - 1;
45
+ let fieldSchema = '';
46
+ // Base type
47
+ switch (field.type) {
48
+ case 'string':
49
+ fieldSchema = 'k.string()';
50
+ break;
51
+ case 'number':
52
+ fieldSchema = 'k.number()';
53
+ break;
54
+ case 'boolean':
55
+ fieldSchema = 'k.boolean()';
56
+ break;
57
+ case 'date':
58
+ fieldSchema = 'k.date()';
59
+ break;
60
+ case 'email':
61
+ fieldSchema = 'k.string().email()';
62
+ break;
63
+ case 'array':
64
+ fieldSchema = 'k.array(k.string())';
65
+ break;
66
+ case 'object':
67
+ fieldSchema = 'k.object({})';
68
+ break;
69
+ default:
70
+ fieldSchema = 'k.string()';
71
+ }
72
+ // Add rules
73
+ if (field.rules && field.rules.length > 0) {
74
+ field.rules.forEach(rule => {
75
+ switch (rule.type) {
76
+ case 'minLength':
77
+ fieldSchema += `\n .minLength(${rule.value}, '${rule.message || `Must be at least ${rule.value} characters`}')`;
78
+ break;
79
+ case 'maxLength':
80
+ fieldSchema += `\n .maxLength(${rule.value}, '${rule.message || `Must not exceed ${rule.value} characters`}')`;
81
+ break;
82
+ case 'min':
83
+ fieldSchema += `\n .min(${rule.value}, '${rule.message || `Must be at least ${rule.value}`}')`;
84
+ break;
85
+ case 'max':
86
+ fieldSchema += `\n .max(${rule.value}, '${rule.message || `Must not exceed ${rule.value}`}')`;
87
+ break;
88
+ case 'email':
89
+ fieldSchema += `\n .email('${rule.message || 'Must be a valid email'}')`;
90
+ break;
91
+ case 'regex':
92
+ fieldSchema += `\n .regex(${rule.value}, '${rule.message || 'Invalid format'}')`;
93
+ break;
94
+ }
95
+ });
96
+ }
97
+ // Add async validator
98
+ if (field.asyncValidator && addAsyncValidators) {
99
+ fieldSchema += `\n .asyncRefine(${field.name}UniqueValidator)`;
100
+ }
101
+ // Add optional
102
+ if (!field.required) {
103
+ fieldSchema += `\n .optional()`;
104
+ }
105
+ content += ` ${field.name}: ${fieldSchema}${isLast ? '' : ','}\n`;
106
+ });
107
+ content += `});\n\n`;
108
+ // Generate type
109
+ content += `/**\n * Inferred TypeScript type from schema\n */\n`;
110
+ content += `export type ${pascalName}Data = kataxInfer<typeof ${camelName}Schema>;\n\n`;
111
+ // Generate ID schema for GET/DELETE operations
112
+ if (config.method === 'GET' || config.method === 'DELETE' || config.method === 'PUT' || config.method === 'PATCH') {
113
+ content += `/**\n * Schema for ${name} ID validation\n */\n`;
114
+ content += `export const ${camelName}IdSchema = k.string()\n`;
115
+ content += ` .minLength(1, 'ID is required')\n`;
116
+ content += ` .regex(/^[0-9a-fA-F-]{36}$|^\\d+$/, 'ID must be a valid UUID or integer');\n\n`;
117
+ content += `export type ${pascalName}IdType = kataxInfer<typeof ${camelName}IdSchema>;\n\n`;
118
+ }
119
+ // Generate validation functions (ValidationResult is imported from api.utils)
120
+ content += `/**\n * Validate ${name} data\n */\n`;
121
+ content += `export async function validate${pascalName}(data: unknown): Promise<ValidationResult<${pascalName}Data>> {\n`;
122
+ content += ` const result = ${addAsyncValidators ? 'await ' : ''}${camelName}Schema.safeParse${addAsyncValidators ? 'Async' : ''}(data);\n\n`;
123
+ content += ` if (!result.success) {\n`;
124
+ content += ` const errors = result.issues.map(issue => ({\n`;
125
+ content += ` field: issue.path.join('.'),\n`;
126
+ content += ` message: issue.message\n`;
127
+ content += ` }));\n\n`;
128
+ content += ` return {\n`;
129
+ content += ` isValid: false,\n`;
130
+ content += ` errors\n`;
131
+ content += ` };\n`;
132
+ content += ` }\n\n`;
133
+ content += ` return {\n`;
134
+ content += ` isValid: true,\n`;
135
+ content += ` data: result.data\n`;
136
+ content += ` };\n`;
137
+ content += `}\n`;
138
+ // Generate ID validation function for GET/DELETE
139
+ if (config.method === 'GET' || config.method === 'DELETE' || config.method === 'PUT' || config.method === 'PATCH') {
140
+ content += `\n/**\n * Validate ${name} ID\n */\n`;
141
+ content += `export async function validate${pascalName}Id(id: string): Promise<ValidationResult<${pascalName}IdType>> {\n`;
142
+ content += ` const result = ${camelName}IdSchema.safeParse(id);\n\n`;
143
+ content += ` if (!result.success) {\n`;
144
+ content += ` const errors = result.issues.map(issue => ({\n`;
145
+ content += ` field: issue.path.join('.') || 'id',\n`;
146
+ content += ` message: issue.message\n`;
147
+ content += ` }));\n\n`;
148
+ content += ` return {\n`;
149
+ content += ` isValid: false,\n`;
150
+ content += ` errors\n`;
151
+ content += ` };\n`;
152
+ content += ` }\n\n`;
153
+ content += ` return {\n`;
154
+ content += ` isValid: true,\n`;
155
+ content += ` data: result.data\n`;
156
+ content += ` };\n`;
157
+ content += `}\n`;
158
+ }
159
+ return content;
160
+ }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Katax CLI - API Generator with TypeScript and katax-core validation
4
+ *
5
+ * A CLI tool for generating Express REST APIs with TypeScript,
6
+ * integrated with katax-core for robust schema validation.
7
+ */
8
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Katax CLI - API Generator with TypeScript and katax-core validation
4
+ *
5
+ * A CLI tool for generating Express REST APIs with TypeScript,
6
+ * integrated with katax-core for robust schema validation.
7
+ */
8
+ import { Command } from 'commander';
9
+ import chalk from 'chalk';
10
+ import { readFileSync } from 'fs';
11
+ import { fileURLToPath } from 'url';
12
+ import { dirname, join } from 'path';
13
+ import { initCommand } from './commands/init.js';
14
+ import { addEndpointCommand } from './commands/add-endpoint.js';
15
+ import { generateCrudCommand } from './commands/generate-crud.js';
16
+ import { infoCommand } from './commands/info.js';
17
+ import { setVerbose, setColorMode } from './utils/logger.js';
18
+ // Get version from package.json
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
21
+ const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
22
+ const version = packageJson.version;
23
+ const program = new Command();
24
+ program
25
+ .name('katax')
26
+ .description(chalk.blue('🚀 Generate Express APIs with TypeScript and katax-core validation'))
27
+ .version(version, '-v, --version', 'Output the current version');
28
+ // Init command - Initialize new API project
29
+ program
30
+ .command('init [project-name]')
31
+ .description('Initialize a new Express API project with TypeScript')
32
+ .option('-f, --force', 'Overwrite existing project')
33
+ .action(initCommand);
34
+ // Add command - Add resources to project
35
+ const addCommand = program
36
+ .command('add')
37
+ .description('Add resources to your project');
38
+ addCommand
39
+ .command('endpoint <name>')
40
+ .description('Add a new endpoint with validation')
41
+ .option('-m, --method <method>', 'HTTP method (GET, POST, PUT, DELETE)', 'POST')
42
+ .option('-p, --path <path>', 'Route path')
43
+ .action(addEndpointCommand);
44
+ // Generate command - Generate complete resources
45
+ const generateCommand = program
46
+ .command('generate')
47
+ .aliases(['gen', 'g'])
48
+ .description('Generate complete resources');
49
+ generateCommand
50
+ .command('crud <resource-name>')
51
+ .description('Generate a complete CRUD resource')
52
+ .option('--no-auth', 'Skip authentication middleware')
53
+ .action(generateCrudCommand);
54
+ // Info command - Show project structure
55
+ program
56
+ .command('info')
57
+ .aliases(['status', 'ls'])
58
+ .description('Show current project structure and routes')
59
+ .action(infoCommand);
60
+ // Global options
61
+ program
62
+ .option('--no-color', 'Disable colored output')
63
+ .option('--verbose', 'Enable verbose logging');
64
+ // Parse global options
65
+ program.hook('preAction', (thisCommand) => {
66
+ const opts = thisCommand.optsWithGlobals();
67
+ if (opts.verbose)
68
+ setVerbose(true);
69
+ if (opts.color === false)
70
+ setColorMode(false);
71
+ });
72
+ // Show help after error and suggestions
73
+ program.showHelpAfterError('(add --help for additional information)');
74
+ program.showSuggestionAfterError(true);
75
+ // Add examples to help
76
+ program.addHelpText('after', `
77
+ ${chalk.bold('Examples:')}
78
+ ${chalk.gray('# Initialize a new API project')}
79
+ $ katax init my-api
80
+
81
+ ${chalk.gray('# Add a single endpoint')}
82
+ $ katax add endpoint users
83
+
84
+ ${chalk.gray('# Generate a complete CRUD resource')}
85
+ $ katax generate crud products
86
+
87
+ ${chalk.gray('# View project structure')}
88
+ $ katax info
89
+
90
+ ${chalk.gray('# Initialize with options')}
91
+ $ katax init my-api --force
92
+
93
+ ${chalk.bold('Documentation:')}
94
+ ${chalk.cyan('https://github.com/LOPIN6FARRIER/katax-cli#readme')}
95
+ `);
96
+ // Parse command line arguments
97
+ program.parse(process.argv);
98
+ // Show help if no command provided
99
+ if (!process.argv.slice(2).length) {
100
+ program.outputHelp();
101
+ }
@@ -0,0 +1,50 @@
1
+ export interface ProjectConfig {
2
+ name: string;
3
+ description?: string;
4
+ type: 'rest-api' | 'graphql';
5
+ typescript: boolean;
6
+ database?: 'postgresql' | 'mysql' | 'mongodb' | 'none';
7
+ authentication?: 'jwt' | 'none';
8
+ validation: 'katax-core' | 'none';
9
+ orm?: 'none' | 'prisma' | 'typeorm';
10
+ port: number;
11
+ dbConfig?: {
12
+ host?: string;
13
+ port?: string;
14
+ user?: string;
15
+ password?: string;
16
+ database?: string;
17
+ useAuth?: boolean;
18
+ };
19
+ }
20
+ export interface EndpointConfig {
21
+ name: string;
22
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
23
+ path: string;
24
+ addValidation: boolean;
25
+ fields?: FieldConfig[];
26
+ addAsyncValidators: boolean;
27
+ dbOperations?: ('create' | 'read' | 'update' | 'delete')[];
28
+ }
29
+ export interface FieldConfig {
30
+ name: string;
31
+ type: 'string' | 'number' | 'boolean' | 'date' | 'email' | 'array' | 'object';
32
+ required: boolean;
33
+ rules?: ValidationRule[];
34
+ asyncValidator?: {
35
+ type: 'unique' | 'exists' | 'custom';
36
+ table?: string;
37
+ column?: string;
38
+ };
39
+ }
40
+ export interface ValidationRule {
41
+ type: 'minLength' | 'maxLength' | 'min' | 'max' | 'email' | 'regex' | 'oneOf' | 'custom';
42
+ value?: any;
43
+ message?: string;
44
+ }
45
+ export interface CRUDConfig {
46
+ resourceName: string;
47
+ tableName?: string;
48
+ fields: FieldConfig[];
49
+ addAuth: boolean;
50
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Get the templates directory path
3
+ */
4
+ export declare function getTemplatesDir(): string;
5
+ /**
6
+ * Render an EJS template
7
+ */
8
+ export declare function renderTemplate(templatePath: string, data: any): Promise<string>;
9
+ /**
10
+ * Copy template file to destination
11
+ */
12
+ export declare function copyTemplate(templatePath: string, destinationPath: string, data?: any): Promise<void>;
13
+ /**
14
+ * Write a file with content
15
+ */
16
+ export declare function writeFile(filePath: string, content: string): Promise<void>;
17
+ /**
18
+ * Check if directory exists
19
+ */
20
+ export declare function directoryExists(dirPath: string): boolean;
21
+ /**
22
+ * Check if file exists
23
+ */
24
+ export declare function fileExists(filePath: string): boolean;
25
+ /**
26
+ * Create directory if it doesn't exist
27
+ */
28
+ export declare function ensureDir(dirPath: string): Promise<void>;
29
+ /**
30
+ * Convert string to PascalCase
31
+ */
32
+ export declare function toPascalCase(str: string): string;
33
+ /**
34
+ * Convert string to camelCase
35
+ */
36
+ export declare function toCamelCase(str: string): string;
37
+ /**
38
+ * Convert string to kebab-case
39
+ */
40
+ export declare function toKebabCase(str: string): string;
@@ -0,0 +1,89 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import ejs from 'ejs';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname } from 'path';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ /**
9
+ * Get the templates directory path
10
+ */
11
+ export function getTemplatesDir() {
12
+ // In development: ../../templates
13
+ // In production: ../templates (dist folder)
14
+ const devPath = path.join(__dirname, '..', '..', 'templates');
15
+ const prodPath = path.join(__dirname, '..', 'templates');
16
+ return fs.existsSync(devPath) ? devPath : prodPath;
17
+ }
18
+ /**
19
+ * Render an EJS template
20
+ */
21
+ export async function renderTemplate(templatePath, data) {
22
+ const fullPath = path.join(getTemplatesDir(), templatePath);
23
+ const templateContent = await fs.readFile(fullPath, 'utf-8');
24
+ return ejs.render(templateContent, data);
25
+ }
26
+ /**
27
+ * Copy template file to destination
28
+ */
29
+ export async function copyTemplate(templatePath, destinationPath, data) {
30
+ const sourcePath = path.join(getTemplatesDir(), templatePath);
31
+ await fs.ensureDir(path.dirname(destinationPath));
32
+ if (data) {
33
+ const rendered = await renderTemplate(templatePath, data);
34
+ await fs.writeFile(destinationPath, rendered, 'utf-8');
35
+ }
36
+ else {
37
+ await fs.copy(sourcePath, destinationPath);
38
+ }
39
+ }
40
+ /**
41
+ * Write a file with content
42
+ */
43
+ export async function writeFile(filePath, content) {
44
+ await fs.ensureDir(path.dirname(filePath));
45
+ await fs.writeFile(filePath, content, 'utf-8');
46
+ }
47
+ /**
48
+ * Check if directory exists
49
+ */
50
+ export function directoryExists(dirPath) {
51
+ return fs.existsSync(dirPath);
52
+ }
53
+ /**
54
+ * Check if file exists
55
+ */
56
+ export function fileExists(filePath) {
57
+ return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
58
+ }
59
+ /**
60
+ * Create directory if it doesn't exist
61
+ */
62
+ export async function ensureDir(dirPath) {
63
+ await fs.ensureDir(dirPath);
64
+ }
65
+ /**
66
+ * Convert string to PascalCase
67
+ */
68
+ export function toPascalCase(str) {
69
+ return str
70
+ .split(/[-_\s]+/)
71
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
72
+ .join('');
73
+ }
74
+ /**
75
+ * Convert string to camelCase
76
+ */
77
+ export function toCamelCase(str) {
78
+ const pascal = toPascalCase(str);
79
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
80
+ }
81
+ /**
82
+ * Convert string to kebab-case
83
+ */
84
+ export function toKebabCase(str) {
85
+ return str
86
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
87
+ .replace(/[\s_]+/g, '-')
88
+ .toLowerCase();
89
+ }
@@ -0,0 +1,10 @@
1
+ export declare function setVerbose(enabled: boolean): void;
2
+ export declare function setColorMode(enabled: boolean): void;
3
+ export declare function success(message: string): void;
4
+ export declare function error(message: string): void;
5
+ export declare function warning(message: string): void;
6
+ export declare function info(message: string): void;
7
+ export declare function verbose(message: string): void;
8
+ export declare function gray(message: string): void;
9
+ export declare function title(message: string): void;
10
+ export declare function code(message: string): void;
@@ -0,0 +1,38 @@
1
+ import chalk from 'chalk';
2
+ let verboseMode = false;
3
+ let colorMode = true;
4
+ export function setVerbose(enabled) {
5
+ verboseMode = enabled;
6
+ }
7
+ export function setColorMode(enabled) {
8
+ colorMode = enabled;
9
+ }
10
+ function applyColor(color, text) {
11
+ return colorMode ? color(text) : text;
12
+ }
13
+ export function success(message) {
14
+ console.log(applyColor(chalk.green, `✅ ${message}`));
15
+ }
16
+ export function error(message) {
17
+ console.log(applyColor(chalk.red, `❌ ${message}`));
18
+ }
19
+ export function warning(message) {
20
+ console.log(applyColor(chalk.yellow, `⚠️ ${message}`));
21
+ }
22
+ export function info(message) {
23
+ console.log(applyColor(chalk.blue, `ℹ️ ${message}`));
24
+ }
25
+ export function verbose(message) {
26
+ if (verboseMode) {
27
+ console.log(applyColor(chalk.gray, `[VERBOSE] ${message}`));
28
+ }
29
+ }
30
+ export function gray(message) {
31
+ console.log(applyColor(chalk.gray, message));
32
+ }
33
+ export function title(message) {
34
+ console.log(applyColor(chalk.bold.cyan, `\n${message}\n`));
35
+ }
36
+ export function code(message) {
37
+ console.log(applyColor(chalk.bgBlack.white, ` ${message} `));
38
+ }
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "katax-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool to generate Express APIs with TypeScript and katax-core validation",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "katax": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "templates",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18.0.0"
18
+ },
19
+ "scripts": {
20
+ "dev": "tsx watch src/index.ts",
21
+ "build": "tsc && npm run copy-templates",
22
+ "copy-templates": "copyfiles -u 1 \"templates/**/*\" dist",
23
+ "start": "node dist/index.js",
24
+ "clean": "rimraf dist",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "keywords": [
28
+ "api",
29
+ "generator",
30
+ "express",
31
+ "typescript",
32
+ "validation",
33
+ "katax-core",
34
+ "cli",
35
+ "rest-api",
36
+ "crud"
37
+ ],
38
+ "author": "Vinicio Esparza",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/LOPIN6FARRIER/katax-cli.git"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/LOPIN6FARRIER/katax-cli/issues"
46
+ },
47
+ "homepage": "https://github.com/LOPIN6FARRIER/katax-cli#readme",
48
+ "dependencies": {
49
+ "chalk": "^5.3.0",
50
+ "commander": "^12.1.0",
51
+ "ejs": "^3.1.10",
52
+ "execa": "^9.5.2",
53
+ "fs-extra": "^11.2.0",
54
+ "inquirer": "^12.3.0",
55
+ "katax-core": "^1.1.0",
56
+ "ora": "^8.1.1"
57
+ },
58
+ "devDependencies": {
59
+ "@types/ejs": "^3.1.5",
60
+ "@types/fs-extra": "^11.0.4",
61
+ "@types/inquirer": "^9.0.7",
62
+ "@types/node": "^22.10.5",
63
+ "copyfiles": "^2.4.1",
64
+ "rimraf": "^6.0.1",
65
+ "tsx": "^4.19.2",
66
+ "typescript": "^5.7.2"
67
+ }
68
+ }