@objectql/cli 0.2.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/CHANGELOG.md +15 -0
- package/README.md +46 -0
- package/bin/objectql +2 -0
- package/dist/commands/generate.d.ts +1 -0
- package/dist/commands/generate.js +149 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/repl.d.ts +1 -0
- package/dist/commands/repl.js +143 -0
- package/dist/commands/repl.js.map +1 -0
- package/dist/commands/serve.d.ts +4 -0
- package/dist/commands/serve.js +125 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/package.json +30 -0
- package/src/commands/generate.ts +131 -0
- package/src/commands/repl.ts +120 -0
- package/src/commands/serve.ts +94 -0
- package/src/index.ts +47 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import glob from 'fast-glob';
|
|
4
|
+
import * as yaml from 'js-yaml';
|
|
5
|
+
import { ObjectConfig, FieldConfig } from '@objectql/types';
|
|
6
|
+
|
|
7
|
+
export async function generateTypes(sourceDir: string, outputDir: string) {
|
|
8
|
+
console.log(`Searching for objects in ${sourceDir}...`);
|
|
9
|
+
|
|
10
|
+
const files = await glob(['**/*.object.yml', '**/*.object.yaml'], {
|
|
11
|
+
cwd: sourceDir,
|
|
12
|
+
absolute: true,
|
|
13
|
+
ignore: ['**/node_modules/**']
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
if (files.length === 0) {
|
|
17
|
+
console.log('No object files found.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Ensure output dir exists
|
|
22
|
+
if (!fs.existsSync(outputDir)) {
|
|
23
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const indexContent: string[] = [];
|
|
27
|
+
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
30
|
+
try {
|
|
31
|
+
const schema = yaml.load(content) as ObjectConfig;
|
|
32
|
+
if (!schema || !schema.name) continue;
|
|
33
|
+
|
|
34
|
+
const typeName = toPascalCase(schema.name);
|
|
35
|
+
const typeDefinition = generateInterface(typeName, schema);
|
|
36
|
+
|
|
37
|
+
const outPath = path.join(outputDir, `${schema.name}.ts`);
|
|
38
|
+
fs.writeFileSync(outPath, typeDefinition);
|
|
39
|
+
console.log(`Generated ${schema.name}.ts`);
|
|
40
|
+
|
|
41
|
+
indexContent.push(`export * from './${schema.name}';`);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error(`Failed to parse ${file}:`, e);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Generate index.ts
|
|
48
|
+
fs.writeFileSync(path.join(outputDir, 'index.ts'), indexContent.join('\n'));
|
|
49
|
+
console.log(`Generated types in ${outputDir}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function generateInterface(typeName: string, schema: ObjectConfig): string {
|
|
53
|
+
const fields = schema.fields || {};
|
|
54
|
+
const lines = [
|
|
55
|
+
`// Auto-generated by ObjectQL. DO NOT EDIT.`,
|
|
56
|
+
`import { ObjectDoc } from '@objectql/types';`, // Assuming a base type exists or we define it
|
|
57
|
+
``,
|
|
58
|
+
`export interface ${typeName} extends ObjectDoc {`
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
62
|
+
const fieldName = field.name || key;
|
|
63
|
+
const isOptional = !field.required;
|
|
64
|
+
const tsType = mapFieldTypeToTs(field);
|
|
65
|
+
|
|
66
|
+
// Add JSDoc
|
|
67
|
+
if (field.label || field.description) {
|
|
68
|
+
lines.push(` /**`);
|
|
69
|
+
if (field.label) lines.push(` * ${field.label}`);
|
|
70
|
+
if (field.description) lines.push(` * ${field.description}`);
|
|
71
|
+
lines.push(` */`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
lines.push(` ${fieldName}${isOptional ? '?' : ''}: ${tsType};`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
lines.push(`}`);
|
|
78
|
+
lines.push(``);
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function mapFieldTypeToTs(field: FieldConfig): string {
|
|
83
|
+
switch (field.type) {
|
|
84
|
+
case 'text':
|
|
85
|
+
case 'textarea':
|
|
86
|
+
case 'markdown':
|
|
87
|
+
case 'html':
|
|
88
|
+
case 'email':
|
|
89
|
+
case 'phone':
|
|
90
|
+
case 'url':
|
|
91
|
+
case 'password':
|
|
92
|
+
case 'select': // Could be stricter if options are strings
|
|
93
|
+
return 'string';
|
|
94
|
+
|
|
95
|
+
case 'number':
|
|
96
|
+
case 'currency':
|
|
97
|
+
case 'percent':
|
|
98
|
+
case 'auto_number':
|
|
99
|
+
return 'number';
|
|
100
|
+
|
|
101
|
+
case 'boolean':
|
|
102
|
+
return 'boolean';
|
|
103
|
+
|
|
104
|
+
case 'date':
|
|
105
|
+
case 'datetime':
|
|
106
|
+
case 'time':
|
|
107
|
+
return 'Date | string';
|
|
108
|
+
|
|
109
|
+
case 'vector':
|
|
110
|
+
return 'number[]';
|
|
111
|
+
|
|
112
|
+
case 'file':
|
|
113
|
+
case 'image':
|
|
114
|
+
return field.multiple ? 'any[]' : 'any'; // Simplified for now
|
|
115
|
+
|
|
116
|
+
case 'object':
|
|
117
|
+
case 'location':
|
|
118
|
+
return 'any';
|
|
119
|
+
|
|
120
|
+
case 'lookup':
|
|
121
|
+
case 'master_detail':
|
|
122
|
+
return 'string | number'; // The ID
|
|
123
|
+
|
|
124
|
+
default:
|
|
125
|
+
return 'any';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function toPascalCase(str: string): string {
|
|
130
|
+
return str.replace(/(^\w|_\w)/g, m => m.replace('_', '').toUpperCase());
|
|
131
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as repl from 'repl';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import { ObjectQL } from '@objectql/core';
|
|
5
|
+
import { register } from 'ts-node';
|
|
6
|
+
|
|
7
|
+
export async function startRepl(configPath?: string) {
|
|
8
|
+
const cwd = process.cwd();
|
|
9
|
+
|
|
10
|
+
// Register ts-node to handle TS config loading
|
|
11
|
+
register({
|
|
12
|
+
transpileOnly: true,
|
|
13
|
+
compilerOptions: {
|
|
14
|
+
module: "commonjs"
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// 1. Resolve Config File
|
|
19
|
+
let configFile = configPath;
|
|
20
|
+
if (!configFile) {
|
|
21
|
+
const potentialFiles = ['objectql.config.ts', 'objectql.config.js'];
|
|
22
|
+
for (const file of potentialFiles) {
|
|
23
|
+
if (fs.existsSync(path.join(cwd, file))) {
|
|
24
|
+
configFile = file;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!configFile) {
|
|
31
|
+
console.error("ā No configuration file found (objectql.config.ts/js).");
|
|
32
|
+
console.log("Please create one that exports an ObjectQL instance.");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`š Loading configuration from ${configFile}...`);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const configModule = require(path.join(cwd, configFile));
|
|
40
|
+
// Support default export or named export 'app' or 'objectql' or 'db'
|
|
41
|
+
const app = configModule.default || configModule.app || configModule.objectql || configModule.db;
|
|
42
|
+
|
|
43
|
+
if (!(app instanceof ObjectQL)) {
|
|
44
|
+
console.error("ā The config file must export an instance of 'ObjectQL' as default or 'app'/'db'.");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. Init ObjectQL
|
|
49
|
+
await app.init();
|
|
50
|
+
console.log("ā
ObjectQL Initialized.");
|
|
51
|
+
|
|
52
|
+
// 3. Start REPL
|
|
53
|
+
const r = repl.start({
|
|
54
|
+
prompt: 'objectql> ',
|
|
55
|
+
useColors: true
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Enable Auto-Await for Promises
|
|
59
|
+
const defaultEval = r.eval;
|
|
60
|
+
(r as any).eval = (cmd: string, context: any, filename: string, callback: any) => {
|
|
61
|
+
defaultEval.call(r, cmd, context, filename, async (err: Error | null, result: any) => {
|
|
62
|
+
if (err) return callback(err, null);
|
|
63
|
+
if (result && typeof result.then === 'function') {
|
|
64
|
+
try {
|
|
65
|
+
const value = await result;
|
|
66
|
+
callback(null, value);
|
|
67
|
+
} catch (e: any) {
|
|
68
|
+
callback(e, null);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
callback(null, result);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// 4. Inject Context
|
|
77
|
+
r.context.app = app;
|
|
78
|
+
r.context.db = app; // Alias for db
|
|
79
|
+
r.context.object = (name: string) => app.getObject(name);
|
|
80
|
+
|
|
81
|
+
// Helper to get a repo quickly: tasks.find() instead of app.object('tasks').find()
|
|
82
|
+
const objects = app.metadata.list('object');
|
|
83
|
+
for (const obj of objects) {
|
|
84
|
+
// Inject repositories as top-level globals if valid identifiers
|
|
85
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(obj.name)) {
|
|
86
|
+
// We use a getter to lazily create context with system privileges
|
|
87
|
+
Object.defineProperty(r.context, obj.name, {
|
|
88
|
+
get: () => {
|
|
89
|
+
// HACK: We need to construct a repository.
|
|
90
|
+
// Since `ObjectRepository` is exported from `@objectql/core`, we can use it if we import it.
|
|
91
|
+
// But `app` is passed from user land. We can rely on `require('@objectql/core')` here.
|
|
92
|
+
const { ObjectRepository } = require('@objectql/core');
|
|
93
|
+
|
|
94
|
+
const replContext: any = {
|
|
95
|
+
roles: ['admin'],
|
|
96
|
+
isSystem: true,
|
|
97
|
+
userId: 'REPL'
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
replContext.object = (n: string) => new ObjectRepository(n, replContext, app);
|
|
101
|
+
replContext.transaction = async (cb: any) => cb(replContext);
|
|
102
|
+
replContext.sudo = () => replContext;
|
|
103
|
+
|
|
104
|
+
return new ObjectRepository(obj.name, replContext, app);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(`\nAvailable Objects: ${objects.map((o: any) => o.name).join(', ')}`);
|
|
111
|
+
console.log(`Usage: tasks.find() (Auto-await enabled)`);
|
|
112
|
+
|
|
113
|
+
// Fix for REPL sometimes not showing prompt immediately
|
|
114
|
+
r.displayPrompt();
|
|
115
|
+
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error("Failed to load or start:", error);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ObjectQL } from '@objectql/core';
|
|
2
|
+
import { KnexDriver } from '@objectql/driver-knex';
|
|
3
|
+
import { createNodeHandler } from '@objectql/server';
|
|
4
|
+
import { createServer } from 'http';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
const CONSOLE_HTML = `
|
|
9
|
+
<!DOCTYPE html>
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="utf-8" />
|
|
13
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
14
|
+
<title>ObjectQL Swagger UI</title>
|
|
15
|
+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
|
|
16
|
+
<style>
|
|
17
|
+
body { margin: 0; padding: 0; }
|
|
18
|
+
</style>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<div id="swagger-ui"></div>
|
|
22
|
+
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" crossorigin></script>
|
|
23
|
+
<script>
|
|
24
|
+
window.onload = () => {
|
|
25
|
+
window.ui = SwaggerUIBundle({
|
|
26
|
+
url: '/openapi.json',
|
|
27
|
+
dom_id: '#swagger-ui',
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
</script>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
export async function serve(options: { port: number; dir: string }) {
|
|
36
|
+
console.log(chalk.blue('Starting ObjectQL Dev Server...'));
|
|
37
|
+
|
|
38
|
+
const rootDir = path.resolve(process.cwd(), options.dir);
|
|
39
|
+
console.log(chalk.gray(`Loading schema from: ${rootDir}`));
|
|
40
|
+
|
|
41
|
+
// 1. Init ObjectQL with in-memory SQLite for Dev
|
|
42
|
+
const app = new ObjectQL({
|
|
43
|
+
datasources: {
|
|
44
|
+
default: new KnexDriver({
|
|
45
|
+
client: 'sqlite3',
|
|
46
|
+
connection: {
|
|
47
|
+
filename: ':memory:' // Or local file './dev.db'
|
|
48
|
+
},
|
|
49
|
+
useNullAsDefault: true
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// 2. Load Schema
|
|
55
|
+
try {
|
|
56
|
+
app.loadFromDirectory(rootDir);
|
|
57
|
+
await app.init();
|
|
58
|
+
console.log(chalk.green('ā
Schema loaded successfully.'));
|
|
59
|
+
} catch (e: any) {
|
|
60
|
+
console.error(chalk.red('ā Failed to load schema:'), e.message);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. Create Handler
|
|
65
|
+
const internalHandler = createNodeHandler(app);
|
|
66
|
+
|
|
67
|
+
// 4. Start Server
|
|
68
|
+
const server = createServer(async (req, res) => {
|
|
69
|
+
// Serve Swagger UI
|
|
70
|
+
if (req.method === 'GET' && (req.url === '/swagger' || req.url === '/swagger/')) {
|
|
71
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
72
|
+
res.end(CONSOLE_HTML);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Redirect / to /swagger for better DX
|
|
77
|
+
if (req.method === 'GET' && req.url === '/') {
|
|
78
|
+
res.writeHead(302, { 'Location': '/swagger' });
|
|
79
|
+
res.end();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Delegate to API Handler
|
|
84
|
+
await internalHandler(req, res);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
server.listen(options.port, () => {
|
|
88
|
+
console.log(chalk.green(`\nš Server ready at http://localhost:${options.port}`));
|
|
89
|
+
console.log(chalk.green(`š Swagger UI: http://localhost:${options.port}/swagger`));
|
|
90
|
+
console.log(chalk.blue(`š OpenAPI Spec: http://localhost:${options.port}/openapi.json`));
|
|
91
|
+
console.log(chalk.gray('\nTry a curl command:'));
|
|
92
|
+
console.log(`curl -X POST http://localhost:${options.port} -H "Content-Type: application/json" -d '{"op": "find", "object": "YourObject", "args": {}}'`);
|
|
93
|
+
});
|
|
94
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { generateTypes } from './commands/generate';
|
|
3
|
+
import { startRepl } from './commands/repl';
|
|
4
|
+
import { serve } from './commands/serve';
|
|
5
|
+
|
|
6
|
+
const program = new Command();
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('objectql')
|
|
10
|
+
.description('ObjectQL CLI tool')
|
|
11
|
+
.version('0.1.0');
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.command('generate')
|
|
15
|
+
.alias('g')
|
|
16
|
+
.description('Generate TypeScript interfaces from ObjectQL schema files')
|
|
17
|
+
.option('-s, --source <path>', 'Source directory containing *.object.yml', '.')
|
|
18
|
+
.option('-o, --output <path>', 'Output directory for generated types', './src/generated')
|
|
19
|
+
.action(async (options) => {
|
|
20
|
+
try {
|
|
21
|
+
await generateTypes(options.source, options.output);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error(error);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
program
|
|
29
|
+
.command('repl')
|
|
30
|
+
.alias('r')
|
|
31
|
+
.description('Start an interactive shell (REPL) to query the database')
|
|
32
|
+
.option('-c, --config <path>', 'Path to objectql.config.ts/js')
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
await startRepl(options.config);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command('serve')
|
|
39
|
+
.alias('s')
|
|
40
|
+
.description('Start a development server')
|
|
41
|
+
.option('-p, --port <number>', 'Port to listen on', '3000')
|
|
42
|
+
.option('-d, --dir <path>', 'Directory containing schema', '.')
|
|
43
|
+
.action(async (options) => {
|
|
44
|
+
await serve({ port: parseInt(options.port), dir: options.dir });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
program.parse();
|