@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.
@@ -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();
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }