Skip to content

Commit da11576

Browse files
committed
Add REPL command to CLI and project example
Introduces a new 'repl' command to the CLI, enabling an interactive shell for querying the database using ObjectQL. Updates documentation with usage instructions, adds a sample configuration and SQLite database to the project-management example, and updates dependencies to include required drivers.
1 parent e54c173 commit da11576

7 files changed

Lines changed: 170 additions & 2 deletions

File tree

docs/guide/cli.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,36 @@ npx objectql generate [options]
3838
npx objectql generate --source ./src/objects --output ./src/types
3939
```
4040

41-
### 2.2 `migration` (Coming Soon)
41+
### 2.2 `repl` (Interactive Shell)
42+
43+
Starts an interactive terminal similar to the MongoDB shell, allowing you to directly query your database using the ObjectQL API.
44+
45+
**Prerequisites:**
46+
47+
You must have an `objectql.config.ts` or `objectql.config.js` file in your project root that exports your configured `ObjectQL` instance (default export or named export `app`).
48+
49+
**Usage:**
50+
51+
```bash
52+
npx objectql repl
53+
```
54+
55+
**Features:**
56+
* **Auto-injected Objects:** All your registered objects are available as global variables (e.g., `await tasks.find()`).
57+
* **Context:** The `app` instance is available as `app`.
58+
* **Sudo Access:** Commands run with system privileges by default in the REPL.
59+
60+
**Example Session:**
61+
62+
```javascript
63+
objectql> await tasks.find({ status: 'todo' })
64+
[ { id: 1, title: 'Fix bug', status: 'todo' } ]
65+
66+
objectql> await projects.create({ name: 'New API' })
67+
{ id: 10, name: 'New API', ... }
68+
```
69+
70+
### 2.3 `migration` (Coming Soon)
4271

4372
Future versions will include migration commands to sync your YAML schema with the database.
4473

20 KB
Binary file not shown.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ObjectQL, MetadataLoader } from '@objectql/core';
2+
import { KnexDriver } from '@objectql/driver-knex';
3+
import * as path from 'path';
4+
5+
const app = new ObjectQL({
6+
datasources: {
7+
default: new KnexDriver({
8+
client: 'sqlite3',
9+
connection: {
10+
filename: path.join(__dirname, 'dev.sqlite3')
11+
},
12+
useNullAsDefault: true
13+
})
14+
}
15+
});
16+
17+
// Load objects from src
18+
const loader = new MetadataLoader(app.metadata);
19+
loader.load(path.join(__dirname, 'src'));
20+
21+
export default app;

examples/project-management/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
"scripts": {
1111
"codegen": "objectql generate -s src -o src",
1212
"build": "npm run codegen && tsc && cp src/*.yml dist/",
13+
"repl": "objectql repl",
1314
"test": "echo \"No tests specified\" && exit 0"
1415
},
1516
"peerDependencies": {
1617
"@objectql/core": "workspace:*",
17-
"@objectql/types": "workspace:*"
18+
"@objectql/types": "workspace:*",
19+
"@objectql/driver-knex": "workspace:*",
20+
"sqlite3": "^5.1.7"
1821
},
1922
"devDependencies": {
2023
"@objectql/core": "workspace:*",

packages/cli/src/commands/repl.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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'
41+
const app = configModule.default || configModule.app || configModule.objectql;
42+
43+
if (!(app instanceof ObjectQL)) {
44+
console.error("❌ The config file must export an instance of 'ObjectQL' as default or 'app'.");
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+
// 4. Inject Context
59+
r.context.app = app;
60+
r.context.object = (name: string) => app.getObject(name);
61+
62+
// Helper to get a repo quickly: tasks.find() instead of app.object('tasks').find()
63+
const objects = app.metadata.list('object');
64+
for (const obj of objects) {
65+
// Inject repositories as top-level globals if valid identifiers
66+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(obj.name)) {
67+
// We use a getter to lazily create context with system privileges
68+
Object.defineProperty(r.context, obj.name, {
69+
get: () => {
70+
// HACK: We need to construct a repository.
71+
// Since `ObjectRepository` is exported from `@objectql/core`, we can use it if we import it.
72+
// But `app` is passed from user land. We can rely on `require('@objectql/core')` here.
73+
const { ObjectRepository } = require('@objectql/core');
74+
75+
const replContext: any = {
76+
roles: ['admin'],
77+
isSystem: true,
78+
userId: 'REPL'
79+
};
80+
81+
replContext.object = (n: string) => new ObjectRepository(n, replContext, app);
82+
replContext.transaction = async (cb: any) => cb(replContext);
83+
replContext.sudo = () => replContext;
84+
85+
return new ObjectRepository(obj.name, replContext, app);
86+
}
87+
});
88+
}
89+
}
90+
91+
console.log(`\nAvailable Objects: ${objects.map((o: any) => o.name).join(', ')}`);
92+
console.log(`Usage: await tasks.find()`);
93+
94+
} catch (error) {
95+
console.error("Failed to load or start:", error);
96+
process.exit(1);
97+
}
98+
}

packages/cli/src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Command } from 'commander';
22
import { generateTypes } from './commands/generate';
3+
import { startRepl } from './commands/repl';
34

45
const program = new Command();
56

@@ -23,4 +24,13 @@ program
2324
}
2425
});
2526

27+
program
28+
.command('repl')
29+
.alias('r')
30+
.description('Start an interactive shell (REPL) to query the database')
31+
.option('-c, --config <path>', 'Path to objectql.config.ts/js')
32+
.action(async (options) => {
33+
await startRepl(options.config);
34+
});
35+
2636
program.parse();

pnpm-lock.yaml

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)