@objectstack/cli 2.0.7 → 3.0.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/.turbo/turbo-build.log +10 -6
- package/CHANGELOG.md +30 -0
- package/dist/bin.js +1988 -487
- package/dist/chunk-CSHQEILI.js +246 -0
- package/dist/chunk-Q74JNWKD.js +248 -0
- package/dist/config-A7BN6UIT.js +11 -0
- package/dist/config-UN34WBHT.js +10 -0
- package/dist/index.js +1058 -449
- package/package.json +9 -9
- package/src/bin.ts +12 -0
- package/src/commands/codemod.ts +178 -0
- package/src/commands/diff.ts +285 -0
- package/src/commands/doctor.ts +385 -3
- package/src/commands/explain.ts +402 -0
- package/src/commands/generate.ts +638 -4
- package/src/commands/lint.ts +303 -0
package/src/commands/generate.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { Command } from 'commander';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
|
-
import { printHeader, printSuccess, printError, printInfo } from '../utils/format.js';
|
|
7
|
+
import { printHeader, printSuccess, printError, printInfo, printStep, createTimer } from '../utils/format.js';
|
|
8
8
|
|
|
9
9
|
// ─── Metadata Type Templates ────────────────────────────────────────
|
|
10
10
|
|
|
@@ -204,11 +204,110 @@ function toSnakeCase(str: string): string {
|
|
|
204
204
|
return str.replace(/[-]/g, '_').replace(/[A-Z]/g, c => `_${c.toLowerCase()}`).replace(/^_/, '');
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
// ─── Field Type Mapping ─────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
const FIELD_TYPE_MAP: Record<string, string> = {
|
|
210
|
+
text: 'string',
|
|
211
|
+
textarea: 'string',
|
|
212
|
+
richtext: 'string',
|
|
213
|
+
html: 'string',
|
|
214
|
+
markdown: 'string',
|
|
215
|
+
number: 'number',
|
|
216
|
+
integer: 'number',
|
|
217
|
+
currency: 'number',
|
|
218
|
+
percent: 'number',
|
|
219
|
+
boolean: 'boolean',
|
|
220
|
+
date: 'string',
|
|
221
|
+
datetime: 'string',
|
|
222
|
+
time: 'string',
|
|
223
|
+
email: 'string',
|
|
224
|
+
phone: 'string',
|
|
225
|
+
url: 'string',
|
|
226
|
+
select: 'string',
|
|
227
|
+
multiselect: 'string[]',
|
|
228
|
+
lookup: 'string',
|
|
229
|
+
master_detail: 'string',
|
|
230
|
+
formula: 'unknown',
|
|
231
|
+
autonumber: 'string',
|
|
232
|
+
json: 'Record<string, unknown>',
|
|
233
|
+
file: 'string',
|
|
234
|
+
image: 'string',
|
|
235
|
+
password: 'string',
|
|
236
|
+
slug: 'string',
|
|
237
|
+
uuid: 'string',
|
|
238
|
+
ip_address: 'string',
|
|
239
|
+
color: 'string',
|
|
240
|
+
rating: 'number',
|
|
241
|
+
geo_point: '{ lat: number; lng: number }',
|
|
242
|
+
vector: 'number[]',
|
|
243
|
+
encrypted: 'string',
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
function fieldTypeToTs(fieldType: string, multiple?: boolean): string {
|
|
247
|
+
const base = FIELD_TYPE_MAP[fieldType] || 'unknown';
|
|
248
|
+
return multiple ? `${base}[]` : base;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function generateTypesFromConfig(config: Record<string, unknown>): string {
|
|
252
|
+
const lines: string[] = [
|
|
253
|
+
'// Auto-generated by ObjectStack CLI — do not edit manually',
|
|
254
|
+
`// Generated at ${new Date().toISOString()}`,
|
|
255
|
+
'',
|
|
256
|
+
"import type { Data } from '@objectstack/spec';",
|
|
257
|
+
'',
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
// Extract objects from config (supports both top-level and nested)
|
|
261
|
+
const objects: Record<string, unknown>[] = [];
|
|
262
|
+
const rawObjects = (config as any).objects ?? (config as any).data?.objects ?? {};
|
|
263
|
+
|
|
264
|
+
if (Array.isArray(rawObjects)) {
|
|
265
|
+
objects.push(...rawObjects);
|
|
266
|
+
} else if (typeof rawObjects === 'object') {
|
|
267
|
+
for (const val of Object.values(rawObjects)) {
|
|
268
|
+
if (val && typeof val === 'object') objects.push(val as Record<string, unknown>);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (objects.length === 0) {
|
|
273
|
+
lines.push('// No objects found in configuration');
|
|
274
|
+
return lines.join('\n') + '\n';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const obj of objects) {
|
|
278
|
+
const name = String(obj.name || 'unknown');
|
|
279
|
+
const typeName = name
|
|
280
|
+
.split('_')
|
|
281
|
+
.map((w: string) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
282
|
+
.join('');
|
|
283
|
+
const fields = (obj.fields ?? {}) as Record<string, Record<string, unknown>>;
|
|
284
|
+
|
|
285
|
+
lines.push(`/** ${String(obj.label || typeName)} record type */`);
|
|
286
|
+
lines.push(`export interface ${typeName}Record {`);
|
|
287
|
+
lines.push(' id: string;');
|
|
288
|
+
|
|
289
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
290
|
+
const fType = String(fieldDef.type || 'text');
|
|
291
|
+
const tsType = fieldTypeToTs(fType, !!fieldDef.multiple);
|
|
292
|
+
const required = fieldDef.required ? '' : '?';
|
|
293
|
+
if (fieldDef.label) {
|
|
294
|
+
lines.push(` /** ${fieldDef.label} */`);
|
|
295
|
+
}
|
|
296
|
+
lines.push(` ${fieldName}${required}: ${tsType};`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
lines.push('}');
|
|
300
|
+
lines.push('');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return lines.join('\n') + '\n';
|
|
304
|
+
}
|
|
305
|
+
|
|
207
306
|
// ─── Command ────────────────────────────────────────────────────────
|
|
208
307
|
|
|
209
|
-
|
|
210
|
-
.alias('
|
|
211
|
-
.description('Generate metadata
|
|
308
|
+
const generateMetadataCommand = new Command('metadata')
|
|
309
|
+
.alias('m')
|
|
310
|
+
.description('Generate metadata scaffold (object, view, action, flow, agent, dashboard, app)')
|
|
212
311
|
.argument('<type>', 'Metadata type to generate')
|
|
213
312
|
.argument('<name>', 'Name for the metadata (use kebab-case)')
|
|
214
313
|
.option('-d, --dir <directory>', 'Target directory (overrides default)')
|
|
@@ -297,3 +396,538 @@ export const generateCommand = new Command('generate')
|
|
|
297
396
|
process.exit(1);
|
|
298
397
|
}
|
|
299
398
|
});
|
|
399
|
+
|
|
400
|
+
const generateTypesCommand = new Command('types')
|
|
401
|
+
.description('Generate TypeScript type definitions from ObjectStack configuration')
|
|
402
|
+
.argument('[config]', 'Configuration file path')
|
|
403
|
+
.option('-o, --output <file>', 'Output file path', 'src/types/objectstack.d.ts')
|
|
404
|
+
.option('--dry-run', 'Show what would be generated without writing files')
|
|
405
|
+
.action(async (configPath, options) => {
|
|
406
|
+
printHeader('Generate Types');
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
const { loadConfig } = await import('../utils/config.js');
|
|
410
|
+
printInfo('Loading configuration...');
|
|
411
|
+
const { config, absolutePath } = await loadConfig(configPath);
|
|
412
|
+
|
|
413
|
+
console.log(` ${chalk.dim('Config:')} ${chalk.white(absolutePath)}`);
|
|
414
|
+
console.log(` ${chalk.dim('Output:')} ${chalk.white(options.output)}`);
|
|
415
|
+
console.log('');
|
|
416
|
+
|
|
417
|
+
const content = generateTypesFromConfig(config as Record<string, unknown>);
|
|
418
|
+
|
|
419
|
+
if (options.dryRun) {
|
|
420
|
+
printInfo('Dry run — no files written');
|
|
421
|
+
console.log('');
|
|
422
|
+
for (const line of content.split('\n')) {
|
|
423
|
+
console.log(chalk.dim(` ${line}`));
|
|
424
|
+
}
|
|
425
|
+
console.log('');
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const outPath = path.resolve(process.cwd(), options.output);
|
|
430
|
+
const outDir = path.dirname(outPath);
|
|
431
|
+
if (!fs.existsSync(outDir)) {
|
|
432
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
433
|
+
}
|
|
434
|
+
fs.writeFileSync(outPath, content);
|
|
435
|
+
printSuccess(`Generated types at ${options.output}`);
|
|
436
|
+
console.log('');
|
|
437
|
+
|
|
438
|
+
} catch (error: any) {
|
|
439
|
+
printError(error.message || String(error));
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// ─── Client SDK Generator ───────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
function generateClientFromConfig(config: Record<string, unknown>): string {
|
|
447
|
+
const lines: string[] = [
|
|
448
|
+
'// Auto-generated by ObjectStack CLI — do not edit manually',
|
|
449
|
+
`// Generated at ${new Date().toISOString()}`,
|
|
450
|
+
'',
|
|
451
|
+
"import type { Data } from '@objectstack/spec';",
|
|
452
|
+
'',
|
|
453
|
+
];
|
|
454
|
+
|
|
455
|
+
const objects: Record<string, unknown>[] = [];
|
|
456
|
+
const rawObjects = (config as any).objects ?? (config as any).data?.objects ?? {};
|
|
457
|
+
|
|
458
|
+
if (Array.isArray(rawObjects)) {
|
|
459
|
+
objects.push(...rawObjects);
|
|
460
|
+
} else if (typeof rawObjects === 'object') {
|
|
461
|
+
for (const val of Object.values(rawObjects)) {
|
|
462
|
+
if (val && typeof val === 'object') objects.push(val as Record<string, unknown>);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (objects.length === 0) {
|
|
467
|
+
lines.push('// No objects found in configuration');
|
|
468
|
+
return lines.join('\n') + '\n';
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Generate type interfaces
|
|
472
|
+
for (const obj of objects) {
|
|
473
|
+
const name = String(obj.name || 'unknown');
|
|
474
|
+
const typeName = name
|
|
475
|
+
.split('_')
|
|
476
|
+
.map((w: string) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
477
|
+
.join('');
|
|
478
|
+
const fields = (obj.fields ?? {}) as Record<string, Record<string, unknown>>;
|
|
479
|
+
|
|
480
|
+
lines.push(`export interface ${typeName}Record {`);
|
|
481
|
+
lines.push(' id: string;');
|
|
482
|
+
|
|
483
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
484
|
+
const fType = String(fieldDef.type || 'text');
|
|
485
|
+
const tsType = fieldTypeToTs(fType, !!fieldDef.multiple);
|
|
486
|
+
const required = fieldDef.required ? '' : '?';
|
|
487
|
+
lines.push(` ${fieldName}${required}: ${tsType};`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
lines.push('}');
|
|
491
|
+
lines.push('');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Generate client class
|
|
495
|
+
lines.push('export class ObjectStackClient {');
|
|
496
|
+
lines.push(' constructor(private baseUrl: string, private headers: Record<string, string> = {}) {}');
|
|
497
|
+
lines.push('');
|
|
498
|
+
lines.push(' private async request<T>(method: string, path: string, body?: unknown): Promise<T> {');
|
|
499
|
+
lines.push(' const res = await fetch(`${this.baseUrl}${path}`, {');
|
|
500
|
+
lines.push(' method,');
|
|
501
|
+
lines.push(" headers: { 'Content-Type': 'application/json', ...this.headers },");
|
|
502
|
+
lines.push(' body: body ? JSON.stringify(body) : undefined,');
|
|
503
|
+
lines.push(' });');
|
|
504
|
+
lines.push(' if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);');
|
|
505
|
+
lines.push(' return res.json() as Promise<T>;');
|
|
506
|
+
lines.push(' }');
|
|
507
|
+
|
|
508
|
+
for (const obj of objects) {
|
|
509
|
+
const name = String(obj.name || 'unknown');
|
|
510
|
+
const typeName = name
|
|
511
|
+
.split('_')
|
|
512
|
+
.map((w: string) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
513
|
+
.join('');
|
|
514
|
+
const endpoint = `/api/${name}`;
|
|
515
|
+
|
|
516
|
+
lines.push('');
|
|
517
|
+
lines.push(` async list${typeName}(): Promise<${typeName}Record[]> {`);
|
|
518
|
+
lines.push(` return this.request<${typeName}Record[]>('GET', '${endpoint}');`);
|
|
519
|
+
lines.push(' }');
|
|
520
|
+
lines.push('');
|
|
521
|
+
lines.push(` async get${typeName}(id: string): Promise<${typeName}Record> {`);
|
|
522
|
+
lines.push(` return this.request<${typeName}Record>('GET', '${endpoint}/\${id}');`);
|
|
523
|
+
lines.push(' }');
|
|
524
|
+
lines.push('');
|
|
525
|
+
lines.push(` async create${typeName}(data: Omit<${typeName}Record, 'id'>): Promise<${typeName}Record> {`);
|
|
526
|
+
lines.push(` return this.request<${typeName}Record>('POST', '${endpoint}', data);`);
|
|
527
|
+
lines.push(' }');
|
|
528
|
+
lines.push('');
|
|
529
|
+
lines.push(` async update${typeName}(id: string, data: Partial<${typeName}Record>): Promise<${typeName}Record> {`);
|
|
530
|
+
lines.push(` return this.request<${typeName}Record>('PATCH', '${endpoint}/\${id}', data);`);
|
|
531
|
+
lines.push(' }');
|
|
532
|
+
lines.push('');
|
|
533
|
+
lines.push(` async delete${typeName}(id: string): Promise<void> {`);
|
|
534
|
+
lines.push(` return this.request<void>('DELETE', '${endpoint}/\${id}');`);
|
|
535
|
+
lines.push(' }');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
lines.push('}');
|
|
539
|
+
lines.push('');
|
|
540
|
+
|
|
541
|
+
return lines.join('\n') + '\n';
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const generateClientCommand = new Command('client')
|
|
545
|
+
.description('Generate a type-safe client SDK from ObjectStack configuration')
|
|
546
|
+
.argument('[config]', 'Configuration file path')
|
|
547
|
+
.option('-o, --output <file>', 'Output file path', 'src/client/objectstack-client.ts')
|
|
548
|
+
.option('--dry-run', 'Show output without writing')
|
|
549
|
+
.action(async (configPath, options) => {
|
|
550
|
+
printHeader('Generate Client SDK');
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
const { loadConfig } = await import('../utils/config.js');
|
|
554
|
+
const timer = createTimer();
|
|
555
|
+
printInfo('Loading configuration...');
|
|
556
|
+
const { config, absolutePath } = await loadConfig(configPath);
|
|
557
|
+
|
|
558
|
+
console.log(` ${chalk.dim('Config:')} ${chalk.white(absolutePath)}`);
|
|
559
|
+
console.log(` ${chalk.dim('Output:')} ${chalk.white(options.output)}`);
|
|
560
|
+
console.log('');
|
|
561
|
+
|
|
562
|
+
printStep('Generating client SDK...');
|
|
563
|
+
const content = generateClientFromConfig(config as Record<string, unknown>);
|
|
564
|
+
|
|
565
|
+
if (options.dryRun) {
|
|
566
|
+
printInfo('Dry run — no files written');
|
|
567
|
+
console.log('');
|
|
568
|
+
for (const line of content.split('\n')) {
|
|
569
|
+
console.log(chalk.dim(` ${line}`));
|
|
570
|
+
}
|
|
571
|
+
console.log('');
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const outPath = path.resolve(process.cwd(), options.output);
|
|
576
|
+
const outDir = path.dirname(outPath);
|
|
577
|
+
if (!fs.existsSync(outDir)) {
|
|
578
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
579
|
+
}
|
|
580
|
+
fs.writeFileSync(outPath, content);
|
|
581
|
+
printSuccess(`Generated client SDK at ${options.output} (${timer.display()})`);
|
|
582
|
+
console.log('');
|
|
583
|
+
|
|
584
|
+
} catch (error: any) {
|
|
585
|
+
printError(error.message || String(error));
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// ─── Migration Generator ────────────────────────────────────────────
|
|
591
|
+
|
|
592
|
+
const FIELD_TYPE_SQL_MAP: Record<string, string> = {
|
|
593
|
+
text: 'VARCHAR(255)',
|
|
594
|
+
textarea: 'TEXT',
|
|
595
|
+
richtext: 'TEXT',
|
|
596
|
+
html: 'TEXT',
|
|
597
|
+
markdown: 'TEXT',
|
|
598
|
+
number: 'DECIMAL(18,2)',
|
|
599
|
+
integer: 'INTEGER',
|
|
600
|
+
currency: 'DECIMAL(18,2)',
|
|
601
|
+
percent: 'DECIMAL(5,2)',
|
|
602
|
+
boolean: 'BOOLEAN',
|
|
603
|
+
date: 'DATE',
|
|
604
|
+
datetime: 'TIMESTAMP',
|
|
605
|
+
time: 'TIME',
|
|
606
|
+
email: 'VARCHAR(255)',
|
|
607
|
+
phone: 'VARCHAR(50)',
|
|
608
|
+
url: 'VARCHAR(2048)',
|
|
609
|
+
select: 'VARCHAR(255)',
|
|
610
|
+
multiselect: 'TEXT',
|
|
611
|
+
lookup: 'VARCHAR(36)',
|
|
612
|
+
master_detail: 'VARCHAR(36)',
|
|
613
|
+
formula: 'TEXT',
|
|
614
|
+
autonumber: 'SERIAL',
|
|
615
|
+
json: 'JSONB',
|
|
616
|
+
file: 'VARCHAR(2048)',
|
|
617
|
+
image: 'VARCHAR(2048)',
|
|
618
|
+
password: 'VARCHAR(255)',
|
|
619
|
+
slug: 'VARCHAR(255)',
|
|
620
|
+
uuid: 'UUID',
|
|
621
|
+
ip_address: 'VARCHAR(45)',
|
|
622
|
+
color: 'VARCHAR(7)',
|
|
623
|
+
rating: 'INTEGER',
|
|
624
|
+
geo_point: 'POINT',
|
|
625
|
+
vector: 'VECTOR',
|
|
626
|
+
encrypted: 'TEXT',
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
function fieldTypeToSql(fieldType: string): string {
|
|
630
|
+
return FIELD_TYPE_SQL_MAP[fieldType] || 'TEXT';
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function generateMigrationSql(config: Record<string, unknown>): string {
|
|
634
|
+
const lines: string[] = [
|
|
635
|
+
'-- Auto-generated by ObjectStack CLI — do not edit manually',
|
|
636
|
+
`-- Generated at ${new Date().toISOString()}`,
|
|
637
|
+
'',
|
|
638
|
+
];
|
|
639
|
+
|
|
640
|
+
const objects: Record<string, unknown>[] = [];
|
|
641
|
+
const rawObjects = (config as any).objects ?? (config as any).data?.objects ?? {};
|
|
642
|
+
|
|
643
|
+
if (Array.isArray(rawObjects)) {
|
|
644
|
+
objects.push(...rawObjects);
|
|
645
|
+
} else if (typeof rawObjects === 'object') {
|
|
646
|
+
for (const val of Object.values(rawObjects)) {
|
|
647
|
+
if (val && typeof val === 'object') objects.push(val as Record<string, unknown>);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (objects.length === 0) {
|
|
652
|
+
lines.push('-- No objects found in configuration');
|
|
653
|
+
return lines.join('\n') + '\n';
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
for (const obj of objects) {
|
|
657
|
+
const tableName = String(obj.name || 'unknown');
|
|
658
|
+
const fields = (obj.fields ?? {}) as Record<string, Record<string, unknown>>;
|
|
659
|
+
|
|
660
|
+
lines.push(`CREATE TABLE IF NOT EXISTS "${tableName}" (`);
|
|
661
|
+
lines.push(' "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),');
|
|
662
|
+
|
|
663
|
+
const fieldLines: string[] = [];
|
|
664
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
665
|
+
const sqlType = fieldTypeToSql(String(fieldDef.type || 'text'));
|
|
666
|
+
const notNull = fieldDef.required ? ' NOT NULL' : '';
|
|
667
|
+
fieldLines.push(` "${fieldName}" ${sqlType}${notNull}`);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
fieldLines.push(' "created_at" TIMESTAMP NOT NULL DEFAULT now()');
|
|
671
|
+
fieldLines.push(' "updated_at" TIMESTAMP NOT NULL DEFAULT now()');
|
|
672
|
+
lines.push(fieldLines.join(',\n'));
|
|
673
|
+
lines.push(');');
|
|
674
|
+
lines.push('');
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return lines.join('\n') + '\n';
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function generateMigrationTs(config: Record<string, unknown>): string {
|
|
681
|
+
const lines: string[] = [
|
|
682
|
+
'// Auto-generated by ObjectStack CLI — do not edit manually',
|
|
683
|
+
`// Generated at ${new Date().toISOString()}`,
|
|
684
|
+
'',
|
|
685
|
+
'export async function up(db: any): Promise<void> {',
|
|
686
|
+
];
|
|
687
|
+
|
|
688
|
+
const objects: Record<string, unknown>[] = [];
|
|
689
|
+
const rawObjects = (config as any).objects ?? (config as any).data?.objects ?? {};
|
|
690
|
+
|
|
691
|
+
if (Array.isArray(rawObjects)) {
|
|
692
|
+
objects.push(...rawObjects);
|
|
693
|
+
} else if (typeof rawObjects === 'object') {
|
|
694
|
+
for (const val of Object.values(rawObjects)) {
|
|
695
|
+
if (val && typeof val === 'object') objects.push(val as Record<string, unknown>);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (objects.length === 0) {
|
|
700
|
+
lines.push(' // No objects found in configuration');
|
|
701
|
+
lines.push('}');
|
|
702
|
+
lines.push('');
|
|
703
|
+
lines.push('export async function down(db: any): Promise<void> {');
|
|
704
|
+
lines.push(' // No objects found in configuration');
|
|
705
|
+
lines.push('}');
|
|
706
|
+
return lines.join('\n') + '\n';
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
for (const obj of objects) {
|
|
710
|
+
const tableName = String(obj.name || 'unknown');
|
|
711
|
+
const fields = (obj.fields ?? {}) as Record<string, Record<string, unknown>>;
|
|
712
|
+
|
|
713
|
+
lines.push(` await db.schema.createTable('${tableName}', (table: any) => {`);
|
|
714
|
+
lines.push(" table.uuid('id').primary().defaultTo(db.fn.uuid());");
|
|
715
|
+
|
|
716
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
717
|
+
const fType = String(fieldDef.type || 'text');
|
|
718
|
+
const required = fieldDef.required ? '.notNullable()' : '.nullable()';
|
|
719
|
+
let colMethod: string;
|
|
720
|
+
|
|
721
|
+
switch (fType) {
|
|
722
|
+
case 'text': case 'email': case 'phone': case 'url': case 'select':
|
|
723
|
+
case 'slug': case 'password': case 'color': case 'ip_address':
|
|
724
|
+
colMethod = `table.string('${fieldName}')`;
|
|
725
|
+
break;
|
|
726
|
+
case 'textarea': case 'richtext': case 'html': case 'markdown':
|
|
727
|
+
case 'formula': case 'encrypted':
|
|
728
|
+
colMethod = `table.text('${fieldName}')`;
|
|
729
|
+
break;
|
|
730
|
+
case 'number': case 'currency': case 'percent':
|
|
731
|
+
colMethod = `table.decimal('${fieldName}')`;
|
|
732
|
+
break;
|
|
733
|
+
case 'integer': case 'rating':
|
|
734
|
+
colMethod = `table.integer('${fieldName}')`;
|
|
735
|
+
break;
|
|
736
|
+
case 'boolean':
|
|
737
|
+
colMethod = `table.boolean('${fieldName}')`;
|
|
738
|
+
break;
|
|
739
|
+
case 'date':
|
|
740
|
+
colMethod = `table.date('${fieldName}')`;
|
|
741
|
+
break;
|
|
742
|
+
case 'datetime':
|
|
743
|
+
colMethod = `table.timestamp('${fieldName}')`;
|
|
744
|
+
break;
|
|
745
|
+
case 'time':
|
|
746
|
+
colMethod = `table.time('${fieldName}')`;
|
|
747
|
+
break;
|
|
748
|
+
case 'json': case 'multiselect':
|
|
749
|
+
colMethod = `table.jsonb('${fieldName}')`;
|
|
750
|
+
break;
|
|
751
|
+
case 'uuid': case 'lookup': case 'master_detail':
|
|
752
|
+
colMethod = `table.uuid('${fieldName}')`;
|
|
753
|
+
break;
|
|
754
|
+
default:
|
|
755
|
+
colMethod = `table.text('${fieldName}')`;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
lines.push(` ${colMethod}${required};`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
lines.push(" table.timestamps(true, true);");
|
|
762
|
+
lines.push(' });');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
lines.push('}');
|
|
766
|
+
lines.push('');
|
|
767
|
+
lines.push('export async function down(db: any): Promise<void> {');
|
|
768
|
+
|
|
769
|
+
// Drop tables in reverse order
|
|
770
|
+
const tableNames = objects.map(o => String(o.name || 'unknown')).reverse();
|
|
771
|
+
for (const tableName of tableNames) {
|
|
772
|
+
lines.push(` await db.schema.dropTableIfExists('${tableName}');`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
lines.push('}');
|
|
776
|
+
|
|
777
|
+
return lines.join('\n') + '\n';
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const generateMigrationCommand = new Command('migration')
|
|
781
|
+
.description('Generate database migration from ObjectStack schema')
|
|
782
|
+
.argument('[config]', 'Configuration file path')
|
|
783
|
+
.option('-o, --output <file>', 'Output file path')
|
|
784
|
+
.option('--format <format>', 'Output format: sql or typescript', 'typescript')
|
|
785
|
+
.option('--dry-run', 'Show output without writing')
|
|
786
|
+
.action(async (configPath, options) => {
|
|
787
|
+
printHeader('Generate Migration');
|
|
788
|
+
|
|
789
|
+
try {
|
|
790
|
+
const { loadConfig } = await import('../utils/config.js');
|
|
791
|
+
const timer = createTimer();
|
|
792
|
+
printInfo('Loading configuration...');
|
|
793
|
+
const { config, absolutePath } = await loadConfig(configPath);
|
|
794
|
+
|
|
795
|
+
const ext = options.format === 'sql' ? 'sql' : 'ts';
|
|
796
|
+
// Format: YYYYMMDDHHmmss (e.g. 20250101120000)
|
|
797
|
+
const timestamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
798
|
+
const defaultOutput = `migrations/${timestamp}_migration.${ext}`;
|
|
799
|
+
const output = options.output || defaultOutput;
|
|
800
|
+
|
|
801
|
+
console.log(` ${chalk.dim('Config:')} ${chalk.white(absolutePath)}`);
|
|
802
|
+
console.log(` ${chalk.dim('Format:')} ${chalk.white(options.format)}`);
|
|
803
|
+
console.log(` ${chalk.dim('Output:')} ${chalk.white(output)}`);
|
|
804
|
+
console.log('');
|
|
805
|
+
|
|
806
|
+
printStep('Generating migration...');
|
|
807
|
+
const content = options.format === 'sql'
|
|
808
|
+
? generateMigrationSql(config as Record<string, unknown>)
|
|
809
|
+
: generateMigrationTs(config as Record<string, unknown>);
|
|
810
|
+
|
|
811
|
+
if (options.dryRun) {
|
|
812
|
+
printInfo('Dry run — no files written');
|
|
813
|
+
console.log('');
|
|
814
|
+
for (const line of content.split('\n')) {
|
|
815
|
+
console.log(chalk.dim(` ${line}`));
|
|
816
|
+
}
|
|
817
|
+
console.log('');
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const outPath = path.resolve(process.cwd(), output);
|
|
822
|
+
const outDir = path.dirname(outPath);
|
|
823
|
+
if (!fs.existsSync(outDir)) {
|
|
824
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
825
|
+
}
|
|
826
|
+
fs.writeFileSync(outPath, content);
|
|
827
|
+
printSuccess(`Generated migration at ${output} (${timer.display()})`);
|
|
828
|
+
console.log('');
|
|
829
|
+
|
|
830
|
+
} catch (error: any) {
|
|
831
|
+
printError(error.message || String(error));
|
|
832
|
+
process.exit(1);
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// ─── JSON Schema Generator ──────────────────────────────────────────
|
|
837
|
+
|
|
838
|
+
const generateSchemaCommand = new Command('schema')
|
|
839
|
+
.description('Generate JSON Schema for objectstack.config.ts (for IDE autocomplete)')
|
|
840
|
+
.option('-o, --output <file>', 'Output file path', 'objectstack.schema.json')
|
|
841
|
+
.option('--dry-run', 'Show output without writing')
|
|
842
|
+
.action(async (options) => {
|
|
843
|
+
printHeader('Generate Schema');
|
|
844
|
+
|
|
845
|
+
try {
|
|
846
|
+
const timer = createTimer();
|
|
847
|
+
printStep('Loading ObjectStackDefinitionSchema...');
|
|
848
|
+
|
|
849
|
+
const { z } = await import('zod');
|
|
850
|
+
const { ObjectStackDefinitionSchema } = await import('@objectstack/spec');
|
|
851
|
+
|
|
852
|
+
printStep('Converting to JSON Schema...');
|
|
853
|
+
const jsonSchema = z.toJSONSchema(ObjectStackDefinitionSchema, {
|
|
854
|
+
target: 'draft-2020-12',
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
// Add metadata
|
|
858
|
+
const schema = {
|
|
859
|
+
...jsonSchema,
|
|
860
|
+
$id: 'https://schema.objectstack.io/objectstack.config.json',
|
|
861
|
+
title: 'ObjectStack Configuration',
|
|
862
|
+
description: 'JSON Schema for objectstack.config.ts — generated from ObjectStackDefinitionSchema',
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
const content = JSON.stringify(schema, null, 2) + '\n';
|
|
866
|
+
|
|
867
|
+
if (options.dryRun) {
|
|
868
|
+
printInfo('Dry run — no files written');
|
|
869
|
+
console.log('');
|
|
870
|
+
console.log(content);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const outPath = path.resolve(process.cwd(), options.output);
|
|
875
|
+
const outDir = path.dirname(outPath);
|
|
876
|
+
if (!fs.existsSync(outDir)) {
|
|
877
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
878
|
+
}
|
|
879
|
+
fs.writeFileSync(outPath, content);
|
|
880
|
+
printSuccess(`Generated JSON Schema at ${options.output} (${timer.display()})`);
|
|
881
|
+
console.log('');
|
|
882
|
+
console.log(chalk.dim(' Usage: Reference in your IDE or editor for autocomplete'));
|
|
883
|
+
console.log(chalk.dim(` Path: ${outPath}`));
|
|
884
|
+
console.log('');
|
|
885
|
+
|
|
886
|
+
} catch (error: any) {
|
|
887
|
+
printError(error.message || String(error));
|
|
888
|
+
process.exit(1);
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
// ─── Main Generate Command ──────────────────────────────────────────
|
|
893
|
+
|
|
894
|
+
export const generateCommand = new Command('generate')
|
|
895
|
+
.alias('g')
|
|
896
|
+
.description('Generate metadata files or TypeScript types')
|
|
897
|
+
.argument('[type]', 'Metadata type to generate (object, view, action, flow, agent, dashboard, app)')
|
|
898
|
+
.argument('[name]', 'Name for the metadata (use kebab-case)')
|
|
899
|
+
.option('-d, --dir <directory>', 'Target directory (overrides default)')
|
|
900
|
+
.option('--dry-run', 'Show what would be created without writing files')
|
|
901
|
+
.addCommand(generateTypesCommand)
|
|
902
|
+
.addCommand(generateClientCommand)
|
|
903
|
+
.addCommand(generateMigrationCommand)
|
|
904
|
+
.addCommand(generateSchemaCommand)
|
|
905
|
+
.action(async (type: string | undefined, name: string | undefined, options) => {
|
|
906
|
+
if (!type) {
|
|
907
|
+
printHeader('Generate');
|
|
908
|
+
console.log(chalk.bold(' Sub-commands:'));
|
|
909
|
+
console.log(` ${chalk.cyan('types'.padEnd(12))} Generate TypeScript type definitions from config`);
|
|
910
|
+
console.log(` ${chalk.cyan('client'.padEnd(12))} Generate a type-safe client SDK from config`);
|
|
911
|
+
console.log(` ${chalk.cyan('migration'.padEnd(12))} Generate database migration from schema`);
|
|
912
|
+
console.log(` ${chalk.cyan('schema'.padEnd(12))} Generate JSON Schema for objectstack.config.ts (IDE autocomplete)`);
|
|
913
|
+
console.log('');
|
|
914
|
+
console.log(chalk.bold(' Metadata types:'));
|
|
915
|
+
for (const [key, gen] of Object.entries(GENERATORS)) {
|
|
916
|
+
console.log(` ${chalk.cyan(key.padEnd(12))} ${chalk.dim(gen.description)}`);
|
|
917
|
+
}
|
|
918
|
+
console.log('');
|
|
919
|
+
console.log(chalk.dim(' Usage: objectstack generate <type> <name>'));
|
|
920
|
+
console.log(chalk.dim(' Usage: objectstack generate types [config]'));
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Delegate to metadata command action
|
|
925
|
+
if (!name) {
|
|
926
|
+
printError('Missing required argument: <name>');
|
|
927
|
+
console.log(chalk.dim(' Usage: objectstack generate <type> <name>'));
|
|
928
|
+
process.exit(1);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Execute metadata generation inline
|
|
932
|
+
await generateMetadataCommand.parseAsync([type, name, ...process.argv.slice(4)], { from: 'user' });
|
|
933
|
+
});
|