@forgebase/cli 0.0.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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/cjs/commands/database.d.ts +3 -0
- package/dist/cjs/commands/database.d.ts.map +1 -0
- package/dist/cjs/commands/database.js +185 -0
- package/dist/cjs/commands/database.js.map +1 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +13 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/utils/ui.d.ts +3 -0
- package/dist/cjs/utils/ui.d.ts.map +1 -0
- package/dist/cjs/utils/ui.js +34 -0
- package/dist/cjs/utils/ui.js.map +1 -0
- package/dist/esm/commands/database.d.ts +3 -0
- package/dist/esm/commands/database.d.ts.map +1 -0
- package/dist/esm/commands/database.js +146 -0
- package/dist/esm/commands/database.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/utils/ui.d.ts +3 -0
- package/dist/esm/utils/ui.d.ts.map +1 -0
- package/dist/esm/utils/ui.js +27 -0
- package/dist/esm/utils/ui.js.map +1 -0
- package/dist/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/tsconfig.esm.tsbuildinfo +1 -0
- package/mock-server.js +39 -0
- package/package.json +49 -0
- package/src/commands/database.ts +204 -0
- package/src/index.ts +14 -0
- package/src/utils/ui.ts +37 -0
- package/test-schema.ts +25 -0
- package/tsconfig.cjs.json +9 -0
- package/tsconfig.esm.json +9 -0
- package/tsconfig.json +19 -0
- package/tsconfig.spec.json +15 -0
package/mock-server.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
|
|
3
|
+
const mockSchema = {
|
|
4
|
+
users: {
|
|
5
|
+
columns: [
|
|
6
|
+
{ name: 'id', dataType: 'serial', isNullable: false },
|
|
7
|
+
{ name: 'email', dataType: 'varchar', isNullable: false },
|
|
8
|
+
{ name: 'age', dataType: 'integer', isNullable: true },
|
|
9
|
+
{ name: 'is_active', dataType: 'boolean', isNullable: false },
|
|
10
|
+
{ name: 'metadata', dataType: 'jsonb', isNullable: true },
|
|
11
|
+
{ name: 'created_at', dataType: 'timestamptz', isNullable: false },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
posts: {
|
|
15
|
+
columns: [
|
|
16
|
+
{ name: 'id', dataType: 'bigserial', isNullable: false },
|
|
17
|
+
{ name: 'title', dataType: 'text', isNullable: false },
|
|
18
|
+
{ name: 'content', dataType: 'text', isNullable: true },
|
|
19
|
+
{ name: 'author_id', dataType: 'integer', isNullable: false },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const server = http.createServer((req, res) => {
|
|
25
|
+
if (req.url === '/schema') {
|
|
26
|
+
// Simulate network latency (3 seconds)
|
|
27
|
+
setTimeout(() => {
|
|
28
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
29
|
+
res.end(JSON.stringify(mockSchema));
|
|
30
|
+
}, 3000);
|
|
31
|
+
} else {
|
|
32
|
+
res.writeHead(404);
|
|
33
|
+
res.end();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
server.listen(3001, () => {
|
|
38
|
+
console.log('Mock schema server running at http://localhost:3001/schema');
|
|
39
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgebase/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "CLI tool for ForgeBase",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"forgebase-cli": "dist/index.js",
|
|
10
|
+
"dbcli": "dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"forgebase",
|
|
14
|
+
"cli",
|
|
15
|
+
"database",
|
|
16
|
+
"schema"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"author": "SOG-web",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@types/figlet": "^1.7.0",
|
|
25
|
+
"axios": "^1.11.0",
|
|
26
|
+
"chalk": "^5.6.2",
|
|
27
|
+
"commander": "^14.0.3",
|
|
28
|
+
"figlet": "^1.10.0",
|
|
29
|
+
"inquirer": "^13.2.4",
|
|
30
|
+
"ora": "^9.3.0",
|
|
31
|
+
"prettier": "^3.6.2"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20.0.0",
|
|
35
|
+
"tslib": "^2.3.0",
|
|
36
|
+
"typescript": "5.8.2",
|
|
37
|
+
"@forgebase/typescript-config": "0.0.0"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"clean": "rm -rf dist",
|
|
41
|
+
"build:esm": "tsc -p tsconfig.esm.json",
|
|
42
|
+
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
43
|
+
"build": "pnpm clean && pnpm build:esm && pnpm build:cjs",
|
|
44
|
+
"dev": "tsc -p tsconfig.esm.json --watch",
|
|
45
|
+
"start": "node dist/index.js",
|
|
46
|
+
"test:mock-server": "node mock-server.js",
|
|
47
|
+
"test:generate": "node dist/esm/index.js database --url http://localhost:3001/schema --output test-schema.ts"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as prettier from 'prettier';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
|
|
9
|
+
const KyselyTypeMapping: Record<string, string> = {
|
|
10
|
+
// Number types
|
|
11
|
+
integer: 'number',
|
|
12
|
+
int2: 'number',
|
|
13
|
+
int4: 'number',
|
|
14
|
+
int8: 'number',
|
|
15
|
+
smallint: 'number',
|
|
16
|
+
bigint: 'number',
|
|
17
|
+
real: 'number',
|
|
18
|
+
'double precision': 'number',
|
|
19
|
+
float4: 'number',
|
|
20
|
+
float8: 'number',
|
|
21
|
+
decimal: 'number',
|
|
22
|
+
numeric: 'number',
|
|
23
|
+
serial: 'number',
|
|
24
|
+
bigserial: 'number',
|
|
25
|
+
|
|
26
|
+
// String types
|
|
27
|
+
varchar: 'string',
|
|
28
|
+
char: 'string',
|
|
29
|
+
text: 'string',
|
|
30
|
+
uuid: 'string',
|
|
31
|
+
date: 'string',
|
|
32
|
+
time: 'string',
|
|
33
|
+
timetz: 'string',
|
|
34
|
+
timestamp: 'string',
|
|
35
|
+
timestamptz: 'string',
|
|
36
|
+
datetime: 'string',
|
|
37
|
+
blob: 'string',
|
|
38
|
+
varbinary: 'string',
|
|
39
|
+
|
|
40
|
+
// Boolean
|
|
41
|
+
boolean: 'boolean',
|
|
42
|
+
|
|
43
|
+
// JSON/Any
|
|
44
|
+
json: 'any',
|
|
45
|
+
jsonb: 'any',
|
|
46
|
+
binary: 'any',
|
|
47
|
+
bytea: 'any',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Default mapping for unknown types
|
|
51
|
+
const DEFAULT_TYPE = 'any';
|
|
52
|
+
|
|
53
|
+
interface ColumnInfo {
|
|
54
|
+
name: string;
|
|
55
|
+
dataType: string;
|
|
56
|
+
isNullable: boolean;
|
|
57
|
+
hasAutoIncrement: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface TableInfo {
|
|
61
|
+
columns: ColumnInfo[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface DatabaseSchema {
|
|
65
|
+
[tableName: string]: TableInfo;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
import { showBanner, confirmAction } from '../utils/ui.js';
|
|
69
|
+
|
|
70
|
+
export const databaseCommand = new Command('database')
|
|
71
|
+
.description('Database management commands')
|
|
72
|
+
.requiredOption('--url <url>', 'URL to the ForgeBase schema endpoint')
|
|
73
|
+
.requiredOption('--output <path>', 'Path to save the generated schema file')
|
|
74
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
75
|
+
.action(async (options) => {
|
|
76
|
+
showBanner(
|
|
77
|
+
'Database Schema Generator',
|
|
78
|
+
'Generates TypeScript definitions from your running database.',
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const confirmed = await confirmAction(
|
|
82
|
+
`Do you want to generate the schema from ${chalk.bold(options.url)} to ${chalk.bold(options.output)}?`,
|
|
83
|
+
options.yes,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (!confirmed) {
|
|
87
|
+
console.log(chalk.yellow('Operation cancelled.'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const spinner = ora('Fetching database schema...').start();
|
|
92
|
+
try {
|
|
93
|
+
// 1. Fetch Schema
|
|
94
|
+
const response = await axios.get<DatabaseSchema>(options.url);
|
|
95
|
+
const schema = response.data;
|
|
96
|
+
|
|
97
|
+
if (!schema || typeof schema !== 'object') {
|
|
98
|
+
throw new Error('Invalid schema response from server');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
spinner.text = 'Generating TypeScript definitions...';
|
|
102
|
+
|
|
103
|
+
// 2. Generate TypeScript Interfaces
|
|
104
|
+
const lines: string[] = [];
|
|
105
|
+
const tableNames: string[] = [];
|
|
106
|
+
|
|
107
|
+
// Add file header
|
|
108
|
+
lines.push('/**');
|
|
109
|
+
lines.push(' * This file was auto-generated by @forgebase/cli.');
|
|
110
|
+
lines.push(' * Do not modify this file manually.');
|
|
111
|
+
lines.push(' */');
|
|
112
|
+
lines.push('');
|
|
113
|
+
|
|
114
|
+
for (const [tableName, tableInfo] of Object.entries(schema)) {
|
|
115
|
+
const interfaceName = toPascalCase(tableName);
|
|
116
|
+
tableNames.push(tableName); // Keep original table name for Schema interface keys
|
|
117
|
+
|
|
118
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
119
|
+
|
|
120
|
+
for (const col of tableInfo.columns) {
|
|
121
|
+
// Map type
|
|
122
|
+
let tsType =
|
|
123
|
+
KyselyTypeMapping[col.dataType.toLowerCase()] || DEFAULT_TYPE;
|
|
124
|
+
|
|
125
|
+
// Handle nullability
|
|
126
|
+
if (col.isNullable) {
|
|
127
|
+
tsType += ' | null';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
lines.push(` ${col.name}: ${tsType};`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
lines.push('}');
|
|
134
|
+
lines.push('');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 3. Generate Schema Interface
|
|
138
|
+
lines.push('export interface Schema {');
|
|
139
|
+
for (const tableName of tableNames) {
|
|
140
|
+
// Map table name to interface name
|
|
141
|
+
// e.g. "users" -> "users: User" (assuming User interface is generated)
|
|
142
|
+
// But commonly tables are plural, interfaces are singular/pascal.
|
|
143
|
+
// Let's use the generated interface name.
|
|
144
|
+
const interfaceName = toPascalCase(tableName);
|
|
145
|
+
lines.push(` ${tableName}: ${interfaceName};`);
|
|
146
|
+
}
|
|
147
|
+
lines.push('}');
|
|
148
|
+
lines.push('');
|
|
149
|
+
|
|
150
|
+
// 4. Format with Prettier
|
|
151
|
+
const rawContent = lines.join('\n');
|
|
152
|
+
const formattedContent = await prettier.format(rawContent, {
|
|
153
|
+
parser: 'typescript',
|
|
154
|
+
singleQuote: true,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// 5. Write to file
|
|
158
|
+
const outputPath = path.resolve(process.cwd(), options.output);
|
|
159
|
+
const outputDir = path.dirname(outputPath);
|
|
160
|
+
|
|
161
|
+
if (!fs.existsSync(outputDir)) {
|
|
162
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fs.writeFileSync(outputPath, formattedContent);
|
|
166
|
+
|
|
167
|
+
spinner.succeed(chalk.green(`Schema generated successfully!`));
|
|
168
|
+
|
|
169
|
+
console.log('');
|
|
170
|
+
console.log(chalk.cyan(' → ') + chalk.bold('Source: ') + options.url);
|
|
171
|
+
console.log(chalk.cyan(' → ') + chalk.bold('Output: ') + outputPath);
|
|
172
|
+
console.log(
|
|
173
|
+
chalk.cyan(' → ') + chalk.bold('Tables: ') + tableNames.length,
|
|
174
|
+
);
|
|
175
|
+
console.log('');
|
|
176
|
+
console.log(chalk.gray(' You can now use this schema with the SDK:'));
|
|
177
|
+
console.log(chalk.blue(' const db = new DatabaseSDK<Schema>(...);'));
|
|
178
|
+
console.log('');
|
|
179
|
+
} catch (error: any) {
|
|
180
|
+
spinner.fail(chalk.red('Failed to generate schema'));
|
|
181
|
+
console.error(chalk.red(error.message));
|
|
182
|
+
if (axios.isAxiosError(error)) {
|
|
183
|
+
console.error(
|
|
184
|
+
chalk.red(
|
|
185
|
+
`Status: ${error.response?.status} ${error.response?.statusText}`,
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
function toPascalCase(str: string): string {
|
|
194
|
+
// Handle snake_case, kebab-case, etc.
|
|
195
|
+
// e.g. user_profiles -> UserProfiles
|
|
196
|
+
return (
|
|
197
|
+
str
|
|
198
|
+
.match(/[a-z0-9]+/gi)
|
|
199
|
+
?.map(
|
|
200
|
+
(word) => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase(),
|
|
201
|
+
)
|
|
202
|
+
.join('') || str
|
|
203
|
+
);
|
|
204
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { databaseCommand } from './commands/database.js';
|
|
4
|
+
|
|
5
|
+
const program = new Command();
|
|
6
|
+
|
|
7
|
+
program
|
|
8
|
+
.name('forgebase-cli')
|
|
9
|
+
.description('CLI tool for ForgeBase')
|
|
10
|
+
.version('0.0.1');
|
|
11
|
+
|
|
12
|
+
program.addCommand(databaseCommand);
|
|
13
|
+
|
|
14
|
+
program.parse();
|
package/src/utils/ui.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import figlet from 'figlet';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export function showBanner(commandName: string, description: string) {
|
|
6
|
+
console.log('');
|
|
7
|
+
console.log(
|
|
8
|
+
chalk.hex('#FF5733')(
|
|
9
|
+
figlet.textSync('ForgeBase CLI', { horizontalLayout: 'full' }),
|
|
10
|
+
),
|
|
11
|
+
);
|
|
12
|
+
console.log('');
|
|
13
|
+
console.log(chalk.bold.cyan(`Command: ${commandName}`));
|
|
14
|
+
console.log(chalk.gray(description));
|
|
15
|
+
console.log(chalk.gray('--------------------------------------------------'));
|
|
16
|
+
console.log('');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function confirmAction(
|
|
20
|
+
message: string,
|
|
21
|
+
skip: boolean,
|
|
22
|
+
): Promise<boolean> {
|
|
23
|
+
if (skip) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { proceed } = await inquirer.prompt([
|
|
28
|
+
{
|
|
29
|
+
type: 'confirm',
|
|
30
|
+
name: 'proceed',
|
|
31
|
+
message: message,
|
|
32
|
+
default: true,
|
|
33
|
+
},
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
return proceed;
|
|
37
|
+
}
|
package/test-schema.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was auto-generated by @forgebase/cli.
|
|
3
|
+
* Do not modify this file manually.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface Users {
|
|
7
|
+
id: number;
|
|
8
|
+
email: string;
|
|
9
|
+
age: number | null;
|
|
10
|
+
is_active: boolean;
|
|
11
|
+
metadata: any | null;
|
|
12
|
+
created_at: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Posts {
|
|
16
|
+
id: number;
|
|
17
|
+
title: string;
|
|
18
|
+
content: string | null;
|
|
19
|
+
author_id: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Schema {
|
|
23
|
+
users: Users;
|
|
24
|
+
posts: Posts;
|
|
25
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@forgebase/typescript-config/base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"composite": true,
|
|
7
|
+
"noImplicitAny": false,
|
|
8
|
+
"target": "ES2020",
|
|
9
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"]
|
|
10
|
+
},
|
|
11
|
+
"include": ["src"],
|
|
12
|
+
"exclude": [
|
|
13
|
+
"node_modules",
|
|
14
|
+
"dist",
|
|
15
|
+
"jest.config.ts",
|
|
16
|
+
"src/**/*.spec.ts",
|
|
17
|
+
"src/**/*.test.ts"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../dist/out-tsc",
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"moduleResolution": "node10",
|
|
7
|
+
"types": ["jest", "node"]
|
|
8
|
+
},
|
|
9
|
+
"include": [
|
|
10
|
+
"jest.config.ts",
|
|
11
|
+
"src/**/*.test.ts",
|
|
12
|
+
"src/**/*.spec.ts",
|
|
13
|
+
"src/**/*.d.ts"
|
|
14
|
+
]
|
|
15
|
+
}
|