@objectql/cli 1.6.0 → 1.7.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,314 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ interface MigrateOptions {
6
+ config?: string;
7
+ dir?: string;
8
+ }
9
+
10
+ interface MigrateCreateOptions {
11
+ name: string;
12
+ dir?: string;
13
+ }
14
+
15
+ interface MigrateStatusOptions {
16
+ config?: string;
17
+ dir?: string;
18
+ }
19
+
20
+ const MIGRATION_TEMPLATE = `import { ObjectQL } from '@objectql/core';
21
+
22
+ /**
23
+ * Migration: {{name}}
24
+ * Created: {{timestamp}}
25
+ */
26
+ export async function up(app: ObjectQL) {
27
+ // TODO: Implement migration logic
28
+ console.log('Running migration: {{name}}');
29
+
30
+ // Example: Add a new field to an object
31
+ // const tasks = app.getObject('tasks');
32
+ // await tasks.updateSchema({
33
+ // fields: {
34
+ // new_field: { type: 'text', label: 'New Field' }
35
+ // }
36
+ // });
37
+ }
38
+
39
+ export async function down(app: ObjectQL) {
40
+ // TODO: Implement rollback logic
41
+ console.log('Rolling back migration: {{name}}');
42
+
43
+ // Example: Remove the field
44
+ // const tasks = app.getObject('tasks');
45
+ // await tasks.updateSchema({
46
+ // fields: {
47
+ // new_field: undefined
48
+ // }
49
+ // });
50
+ }
51
+ `;
52
+
53
+ /**
54
+ * Run pending migrations
55
+ */
56
+ export async function migrate(options: MigrateOptions) {
57
+ const migrationsDir = path.resolve(process.cwd(), options.dir || './migrations');
58
+
59
+ console.log(chalk.blue('šŸ”„ Running migrations...'));
60
+ console.log(chalk.gray(`Migrations directory: ${migrationsDir}\n`));
61
+
62
+ if (!fs.existsSync(migrationsDir)) {
63
+ console.log(chalk.yellow('⚠ No migrations directory found'));
64
+ console.log(chalk.gray('Create one with: objectql migrate:create --name init'));
65
+ return;
66
+ }
67
+
68
+ try {
69
+ // Load ObjectQL instance from config
70
+ const app = await loadObjectQLInstance(options.config);
71
+
72
+ // Get list of migration files
73
+ const files = fs.readdirSync(migrationsDir)
74
+ .filter(f => f.endsWith('.ts') || f.endsWith('.js'))
75
+ .sort();
76
+
77
+ if (files.length === 0) {
78
+ console.log(chalk.yellow('⚠ No migration files found'));
79
+ return;
80
+ }
81
+
82
+ // Get already run migrations
83
+ const migrations = app.getObject('_migrations');
84
+ let runMigrations: string[] = [];
85
+
86
+ try {
87
+ const result = await migrations.find({
88
+ fields: ['name'],
89
+ sort: [['created_at', 'asc']]
90
+ });
91
+ runMigrations = result.records.map((r: any) => r.name);
92
+ } catch (err) {
93
+ // Migrations table doesn't exist yet, create it
94
+ console.log(chalk.gray('Creating migrations tracking table...'));
95
+ await createMigrationsTable(app);
96
+ }
97
+
98
+ // Run pending migrations
99
+ let ranCount = 0;
100
+ for (const file of files) {
101
+ const migrationName = file.replace(/\.(ts|js)$/, '');
102
+
103
+ if (runMigrations.includes(migrationName)) {
104
+ console.log(chalk.gray(`⊘ ${migrationName} (already run)`));
105
+ continue;
106
+ }
107
+
108
+ console.log(chalk.blue(`ā–¶ ${migrationName}`));
109
+
110
+ const migrationPath = path.join(migrationsDir, file);
111
+ const migration = require(migrationPath);
112
+
113
+ if (!migration.up) {
114
+ console.log(chalk.red(` āœ— No 'up' function found`));
115
+ continue;
116
+ }
117
+
118
+ try {
119
+ await migration.up(app);
120
+
121
+ // Record migration
122
+ await migrations.insert({
123
+ name: migrationName,
124
+ run_at: new Date()
125
+ });
126
+
127
+ console.log(chalk.green(` āœ“ Complete`));
128
+ ranCount++;
129
+ } catch (error: any) {
130
+ console.log(chalk.red(` āœ— Failed: ${error.message}`));
131
+ throw error;
132
+ }
133
+ }
134
+
135
+ console.log(chalk.green(`\nāœ… Ran ${ranCount} migration(s)`));
136
+
137
+ } catch (error: any) {
138
+ console.error(chalk.red(`āŒ Migration failed: ${error.message}`));
139
+ process.exit(1);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Create a new migration file with boilerplate code
145
+ */
146
+ export async function migrateCreate(options: MigrateCreateOptions) {
147
+ const { name } = options;
148
+ const migrationsDir = path.resolve(process.cwd(), options.dir || './migrations');
149
+
150
+ // Validate name
151
+ if (!name || !/^[a-z][a-z0-9_]*$/.test(name)) {
152
+ console.error(chalk.red('āŒ Invalid migration name. Use lowercase with underscores (e.g., add_status_field)'));
153
+ process.exit(1);
154
+ }
155
+
156
+ // Create migrations directory if it doesn't exist
157
+ if (!fs.existsSync(migrationsDir)) {
158
+ fs.mkdirSync(migrationsDir, { recursive: true });
159
+ console.log(chalk.gray(`Created directory: ${migrationsDir}`));
160
+ }
161
+
162
+ // Generate filename with timestamp
163
+ const timestamp = new Date().toISOString().replace(/[:\-T.]/g, '').slice(0, 14);
164
+ const filename = `${timestamp}_${name}.ts`;
165
+ const filePath = path.join(migrationsDir, filename);
166
+
167
+ // Create migration file from template
168
+ const content = MIGRATION_TEMPLATE
169
+ .replace(/\{\{name\}\}/g, name)
170
+ .replace(/\{\{timestamp\}\}/g, new Date().toISOString());
171
+
172
+ fs.writeFileSync(filePath, content, 'utf-8');
173
+
174
+ console.log(chalk.green(`āœ… Created migration: ${filename}`));
175
+ console.log(chalk.gray(`Path: ${filePath}`));
176
+ console.log(chalk.gray(`\nNext steps:`));
177
+ console.log(chalk.gray(` 1. Edit the migration file to add your changes`));
178
+ console.log(chalk.gray(` 2. Run: objectql migrate`));
179
+ }
180
+
181
+ /**
182
+ * Show migration status - displays pending and completed migrations
183
+ * @param options - Configuration options including config path and migrations directory
184
+ */
185
+ export async function migrateStatus(options: MigrateStatusOptions) {
186
+ const migrationsDir = path.resolve(process.cwd(), options.dir || './migrations');
187
+
188
+ console.log(chalk.blue('šŸ“Š Migration Status\n'));
189
+
190
+ if (!fs.existsSync(migrationsDir)) {
191
+ console.log(chalk.yellow('⚠ No migrations directory found'));
192
+ return;
193
+ }
194
+
195
+ try {
196
+ // Load ObjectQL instance
197
+ const app = await loadObjectQLInstance(options.config);
198
+
199
+ // Get list of migration files
200
+ const files = fs.readdirSync(migrationsDir)
201
+ .filter(f => f.endsWith('.ts') || f.endsWith('.js'))
202
+ .sort();
203
+
204
+ if (files.length === 0) {
205
+ console.log(chalk.gray('No migration files found'));
206
+ return;
207
+ }
208
+
209
+ // Get run migrations
210
+ const migrations = app.getObject('_migrations');
211
+ let runMigrations: string[] = [];
212
+
213
+ try {
214
+ const result = await migrations.find({
215
+ fields: ['name', 'run_at'],
216
+ sort: [['run_at', 'asc']]
217
+ });
218
+ runMigrations = result.records.map((r: any) => r.name);
219
+ } catch (err) {
220
+ // Migrations table doesn't exist
221
+ runMigrations = [];
222
+ }
223
+
224
+ // Display status
225
+ let pendingCount = 0;
226
+ for (const file of files) {
227
+ const migrationName = file.replace(/\.(ts|js)$/, '');
228
+ const isRun = runMigrations.includes(migrationName);
229
+
230
+ if (isRun) {
231
+ console.log(chalk.green(`āœ“ ${migrationName}`));
232
+ } else {
233
+ console.log(chalk.yellow(`ā—‹ ${migrationName} (pending)`));
234
+ pendingCount++;
235
+ }
236
+ }
237
+
238
+ console.log(chalk.blue(`\nšŸ“Š Summary:`));
239
+ console.log(chalk.gray(`Total migrations: ${files.length}`));
240
+ console.log(chalk.gray(`Run: ${files.length - pendingCount}`));
241
+ console.log(chalk.gray(`Pending: ${pendingCount}`));
242
+
243
+ } catch (error: any) {
244
+ console.error(chalk.red(`āŒ Failed to get status: ${error.message}`));
245
+ process.exit(1);
246
+ }
247
+ }
248
+
249
+ async function loadObjectQLInstance(configPath?: string): Promise<any> {
250
+ const cwd = process.cwd();
251
+
252
+ // Try to load from config file
253
+ let configFile = configPath;
254
+ if (!configFile) {
255
+ const potentialFiles = ['objectql.config.ts', 'objectql.config.js'];
256
+ for (const file of potentialFiles) {
257
+ if (fs.existsSync(path.join(cwd, file))) {
258
+ configFile = file;
259
+ break;
260
+ }
261
+ }
262
+ }
263
+
264
+ if (!configFile) {
265
+ throw new Error('No configuration file found (objectql.config.ts/js)');
266
+ }
267
+
268
+ // Register ts-node for TypeScript support
269
+ try {
270
+ require('ts-node').register({
271
+ transpileOnly: true,
272
+ compilerOptions: {
273
+ module: 'commonjs'
274
+ }
275
+ });
276
+ } catch (err) {
277
+ // ts-node not available, try to load JS directly
278
+ }
279
+
280
+ const configModule = require(path.join(cwd, configFile));
281
+ const app = configModule.default || configModule.app || configModule.objectql || configModule.db;
282
+
283
+ if (!app) {
284
+ throw new Error('Config file must export an ObjectQL instance');
285
+ }
286
+
287
+ await app.init();
288
+ return app;
289
+ }
290
+
291
+ async function createMigrationsTable(app: any) {
292
+ // Create a system object to track migrations
293
+ app.metadata.register('object', {
294
+ name: '_migrations',
295
+ label: 'Migrations',
296
+ system: true,
297
+ fields: {
298
+ name: {
299
+ type: 'text',
300
+ label: 'Migration Name',
301
+ required: true,
302
+ unique: true
303
+ },
304
+ run_at: {
305
+ type: 'datetime',
306
+ label: 'Run At',
307
+ required: true
308
+ }
309
+ }
310
+ });
311
+
312
+ // Sync to database
313
+ await app.getObject('_migrations').sync();
314
+ }
@@ -0,0 +1,268 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import chalk from 'chalk';
4
+ import * as yaml from 'js-yaml';
5
+
6
+ interface NewOptions {
7
+ type: string;
8
+ name: string;
9
+ dir?: string;
10
+ }
11
+
12
+ const METADATA_TYPES = [
13
+ 'object',
14
+ 'view',
15
+ 'form',
16
+ 'page',
17
+ 'action',
18
+ 'hook',
19
+ 'permission',
20
+ 'validation',
21
+ 'workflow',
22
+ 'report',
23
+ 'menu',
24
+ 'data'
25
+ ];
26
+
27
+ const TEMPLATES: Record<string, any> = {
28
+ object: {
29
+ label: '{{label}}',
30
+ fields: {
31
+ name: {
32
+ type: 'text',
33
+ label: 'Name',
34
+ required: true
35
+ }
36
+ }
37
+ },
38
+ view: {
39
+ label: '{{label}} View',
40
+ object: '{{objectName}}',
41
+ columns: [
42
+ { field: 'name', label: 'Name' }
43
+ ]
44
+ },
45
+ form: {
46
+ label: '{{label}} Form',
47
+ object: '{{objectName}}',
48
+ layout: {
49
+ sections: [
50
+ {
51
+ label: 'Basic Information',
52
+ fields: ['name']
53
+ }
54
+ ]
55
+ }
56
+ },
57
+ page: {
58
+ label: '{{label}} Page',
59
+ type: 'standard',
60
+ components: [
61
+ {
62
+ type: 'title',
63
+ text: '{{label}}'
64
+ }
65
+ ]
66
+ },
67
+ action: {
68
+ label: '{{label}} Action',
69
+ object: '{{objectName}}',
70
+ type: 'record',
71
+ handler: 'action_{{name}}'
72
+ },
73
+ permission: {
74
+ label: '{{label}} Permissions',
75
+ object: '{{objectName}}',
76
+ profiles: {
77
+ admin: {
78
+ allow_read: true,
79
+ allow_create: true,
80
+ allow_edit: true,
81
+ allow_delete: true
82
+ },
83
+ user: {
84
+ allow_read: true,
85
+ allow_create: false,
86
+ allow_edit: false,
87
+ allow_delete: false
88
+ }
89
+ }
90
+ },
91
+ validation: {
92
+ label: '{{label}} Validation',
93
+ object: '{{objectName}}',
94
+ rules: [
95
+ {
96
+ name: 'required_name',
97
+ type: 'field',
98
+ field: 'name',
99
+ rule: 'required',
100
+ message: 'Name is required'
101
+ }
102
+ ]
103
+ },
104
+ workflow: {
105
+ label: '{{label}} Workflow',
106
+ object: '{{objectName}}',
107
+ trigger: 'on_create',
108
+ actions: [
109
+ {
110
+ type: 'field_update',
111
+ field: 'status',
112
+ value: 'draft'
113
+ }
114
+ ]
115
+ },
116
+ report: {
117
+ label: '{{label}} Report',
118
+ type: 'tabular',
119
+ object: '{{objectName}}',
120
+ columns: [
121
+ { field: 'name', label: 'Name' }
122
+ ]
123
+ },
124
+ menu: {
125
+ label: '{{label}} Menu',
126
+ items: [
127
+ {
128
+ label: 'Home',
129
+ page: 'home',
130
+ icon: 'home'
131
+ }
132
+ ]
133
+ },
134
+ data: {
135
+ label: '{{label}} Data',
136
+ object: '{{objectName}}',
137
+ records: []
138
+ }
139
+ };
140
+
141
+ export async function newMetadata(options: NewOptions) {
142
+ const { type, name, dir = '.' } = options;
143
+
144
+ // Validate type
145
+ if (!METADATA_TYPES.includes(type)) {
146
+ console.error(chalk.red(`āŒ Unknown metadata type: ${type}`));
147
+ console.log(chalk.gray(`Available types: ${METADATA_TYPES.join(', ')}`));
148
+ process.exit(1);
149
+ }
150
+
151
+ // Validate name
152
+ if (!name || !/^[a-z][a-z0-9_]*$/.test(name)) {
153
+ console.error(chalk.red('āŒ Invalid name. Must be lowercase with underscores (e.g., my_object)'));
154
+ process.exit(1);
155
+ }
156
+
157
+ const targetDir = path.resolve(process.cwd(), dir);
158
+
159
+ // Create directory if it doesn't exist
160
+ if (!fs.existsSync(targetDir)) {
161
+ fs.mkdirSync(targetDir, { recursive: true });
162
+ }
163
+
164
+ const filename = `${name}.${type}.yml`;
165
+ const filePath = path.join(targetDir, filename);
166
+
167
+ // Check if file already exists
168
+ if (fs.existsSync(filePath)) {
169
+ console.error(chalk.red(`āŒ File already exists: ${filePath}`));
170
+ process.exit(1);
171
+ }
172
+
173
+ // Get template and replace placeholders
174
+ let template = TEMPLATES[type];
175
+ const label = nameToLabel(name);
176
+ const objectName = type === 'object' ? name : extractObjectName(name);
177
+
178
+ template = JSON.parse(
179
+ JSON.stringify(template)
180
+ .replace(/\{\{label\}\}/g, label)
181
+ .replace(/\{\{name\}\}/g, name)
182
+ .replace(/\{\{objectName\}\}/g, objectName)
183
+ );
184
+
185
+ // Write YAML file
186
+ const yamlContent = yaml.dump(template, {
187
+ indent: 2,
188
+ lineWidth: 120,
189
+ noRefs: true
190
+ });
191
+
192
+ fs.writeFileSync(filePath, yamlContent, 'utf-8');
193
+
194
+ console.log(chalk.green(`āœ… Created ${filename}`));
195
+ console.log(chalk.gray(` Path: ${filePath}`));
196
+
197
+ // If it's an action or hook, also create the TypeScript implementation file
198
+ if (type === 'action' || type === 'hook') {
199
+ await createTsImplementation(type, name, targetDir);
200
+ }
201
+ }
202
+
203
+ async function createTsImplementation(type: 'action' | 'hook', name: string, dir: string) {
204
+ const filename = `${name}.${type}.ts`;
205
+ const filePath = path.join(dir, filename);
206
+
207
+ if (fs.existsSync(filePath)) {
208
+ console.log(chalk.yellow(`⚠ TypeScript file already exists: ${filename}`));
209
+ return;
210
+ }
211
+
212
+ let template = '';
213
+
214
+ if (type === 'action') {
215
+ template = `import { ActionContext } from '@objectql/types';
216
+
217
+ export async function action_${name}(context: ActionContext) {
218
+ const { record, user } = context;
219
+
220
+ // TODO: Implement action logic
221
+ console.log('Action ${name} triggered for record:', record._id);
222
+
223
+ return {
224
+ success: true,
225
+ message: 'Action completed successfully'
226
+ };
227
+ }
228
+ `;
229
+ } else if (type === 'hook') {
230
+ template = `import { HookContext } from '@objectql/types';
231
+
232
+ export async function beforeInsert(context: HookContext) {
233
+ const { doc } = context;
234
+
235
+ // TODO: Implement before insert logic
236
+ console.log('Before insert hook for ${name}');
237
+
238
+ // Modify doc as needed
239
+ return doc;
240
+ }
241
+
242
+ export async function afterInsert(context: HookContext) {
243
+ const { doc } = context;
244
+
245
+ // TODO: Implement after insert logic
246
+ console.log('After insert hook for ${name}');
247
+ }
248
+ `;
249
+ }
250
+
251
+ fs.writeFileSync(filePath, template, 'utf-8');
252
+ console.log(chalk.green(`āœ… Created ${filename}`));
253
+ console.log(chalk.gray(` Path: ${filePath}`));
254
+ }
255
+
256
+ function nameToLabel(name: string): string {
257
+ return name
258
+ .split('_')
259
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
260
+ .join(' ');
261
+ }
262
+
263
+ function extractObjectName(name: string): string {
264
+ // Try to extract object name from name like "project_status" -> "project"
265
+ // This is a heuristic and might not always be correct
266
+ const parts = name.split('_');
267
+ return parts[0];
268
+ }
@@ -42,7 +42,7 @@ function openBrowser(url: string) {
42
42
  }
43
43
 
44
44
  export async function startStudio(options: { port: number; dir: string, open?: boolean }) {
45
- const port = options.port || 3000;
45
+ const startPort = options.port || 5555;
46
46
  const rootDir = path.resolve(process.cwd(), options.dir || '.');
47
47
 
48
48
  console.log(chalk.blue('Starting ObjectQL Studio...'));
@@ -78,6 +78,49 @@ export async function startStudio(options: { port: number; dir: string, open?: b
78
78
  process.exit(1);
79
79
  }
80
80
 
81
+ // Initialize App if it's a configuration object
82
+ if (typeof (app as any).init !== 'function') {
83
+ const config = app as any;
84
+ console.log(chalk.gray('Configuration object detected. Initializing ObjectQL instance...'));
85
+
86
+ const datasources: any = {};
87
+
88
+ if (config.datasource && config.datasource.default) {
89
+ const dbConfig = config.datasource.default;
90
+ if (dbConfig.type === 'sqlite') {
91
+ try {
92
+ const { KnexDriver } = require('@objectql/driver-sql');
93
+ datasources.default = new KnexDriver({
94
+ client: 'sqlite3',
95
+ connection: {
96
+ filename: dbConfig.filename ? path.resolve(rootDir, dbConfig.filename) : ':memory:'
97
+ },
98
+ useNullAsDefault: true
99
+ });
100
+ } catch (e) {
101
+ console.warn(chalk.yellow('Failed to load @objectql/driver-sql. Ensure it is installed.'));
102
+ }
103
+ }
104
+ }
105
+
106
+ // Fallback to memory if no datasource
107
+ if (!datasources.default) {
108
+ console.warn(chalk.yellow('No valid datasource found. Using in-memory SQLite.'));
109
+ const { KnexDriver } = require('@objectql/driver-sql');
110
+ datasources.default = new KnexDriver({
111
+ client: 'sqlite3',
112
+ connection: { filename: ':memory:' },
113
+ useNullAsDefault: true
114
+ });
115
+ }
116
+
117
+ app = new ObjectQL({ datasources });
118
+
119
+ // Load Schema
120
+ const loader = new ObjectLoader(app.metadata);
121
+ loader.load(rootDir);
122
+ }
123
+
81
124
  // 2. Load Schema & Init
82
125
  try {
83
126
  await app.init();
@@ -254,13 +297,35 @@ export async function startStudio(options: { port: number; dir: string, open?: b
254
297
  res.end('Not Found');
255
298
  });
256
299
 
257
- server.listen(port, () => {
258
- const url = `http://localhost:${port}/studio`;
259
- console.log(chalk.green(`\nšŸš€ Studio running at: ${chalk.bold(url)}`));
260
- console.log(chalk.gray(` API endpoint: http://localhost:${port}/api`));
261
-
262
- if (options.open) {
263
- openBrowser(url);
264
- }
265
- });
300
+ const tryListen = (port: number) => {
301
+ server.removeAllListeners('error');
302
+ server.removeAllListeners('listening'); // Prevent stacking callbacks
303
+
304
+ server.on('error', (e: any) => {
305
+ if (e.code === 'EADDRINUSE') {
306
+ if (port - startPort < 10) {
307
+ console.log(chalk.yellow(`Port ${port} is in use, trying ${port + 1}...`));
308
+ server.close();
309
+ tryListen(port + 1);
310
+ } else {
311
+ console.error(chalk.red(`āŒ Unable to find a free port.`));
312
+ process.exit(1);
313
+ }
314
+ } else {
315
+ console.error(chalk.red('āŒ Server error:'), e);
316
+ }
317
+ });
318
+
319
+ server.listen(port, () => {
320
+ const url = `http://localhost:${port}/studio`;
321
+ console.log(chalk.green(`\nšŸš€ Studio running at: ${chalk.bold(url)}`));
322
+ console.log(chalk.gray(` API endpoint: http://localhost:${port}/api`));
323
+
324
+ if (options.open) {
325
+ openBrowser(url);
326
+ }
327
+ });
328
+ };
329
+
330
+ tryListen(startPort);
266
331
  }