@objectql/cli 1.6.1 → 1.7.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/CHANGELOG.md +26 -5
- package/IMPLEMENTATION_SUMMARY.md +437 -0
- package/README.md +385 -7
- package/USAGE_EXAMPLES.md +804 -0
- package/__tests__/commands.test.ts +153 -0
- package/dist/commands/i18n.d.ts +27 -0
- package/dist/commands/i18n.js +280 -0
- package/dist/commands/i18n.js.map +1 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +202 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/migrate.d.ts +26 -0
- package/dist/commands/migrate.js +301 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/new.d.ts +7 -0
- package/dist/commands/new.js +279 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/serve.js +1 -1
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/studio.js +92 -9
- package/dist/commands/studio.js.map +1 -1
- package/dist/index.js +130 -2
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/commands/i18n.ts +303 -0
- package/src/commands/init.ts +191 -0
- package/src/commands/migrate.ts +314 -0
- package/src/commands/new.ts +268 -0
- package/src/commands/serve.ts +2 -2
- package/src/commands/studio.ts +98 -10
- package/src/index.ts +131 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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
|
+
}
|
package/src/commands/serve.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ObjectQL } from '@objectql/core';
|
|
2
|
-
import {
|
|
2
|
+
import { SqlDriver } from '@objectql/driver-sql';
|
|
3
3
|
import { ObjectLoader } from '@objectql/platform-node';
|
|
4
4
|
import { createNodeHandler } from '@objectql/server';
|
|
5
5
|
import { createServer } from 'http';
|
|
@@ -42,7 +42,7 @@ export async function serve(options: { port: number; dir: string }) {
|
|
|
42
42
|
// 1. Init ObjectQL with in-memory SQLite for Dev
|
|
43
43
|
const app = new ObjectQL({
|
|
44
44
|
datasources: {
|
|
45
|
-
default: new
|
|
45
|
+
default: new SqlDriver({
|
|
46
46
|
client: 'sqlite3',
|
|
47
47
|
connection: {
|
|
48
48
|
filename: ':memory:' // Or local file './dev.db'
|