s3db.js 12.0.1 → 12.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.
- package/README.md +212 -196
- package/dist/s3db.cjs.js +1431 -4001
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1426 -3997
- package/dist/s3db.es.js.map +1 -1
- package/mcp/entrypoint.js +91 -57
- package/package.json +7 -1
- package/src/cli/index.js +954 -43
- package/src/cli/migration-manager.js +270 -0
- package/src/concerns/calculator.js +0 -4
- package/src/concerns/metadata-encoding.js +1 -21
- package/src/concerns/plugin-storage.js +17 -4
- package/src/concerns/typescript-generator.d.ts +171 -0
- package/src/concerns/typescript-generator.js +275 -0
- package/src/database.class.js +171 -28
- package/src/index.js +15 -9
- package/src/plugins/api/index.js +0 -1
- package/src/plugins/api/routes/resource-routes.js +86 -1
- package/src/plugins/api/server.js +79 -3
- package/src/plugins/api/utils/openapi-generator.js +195 -5
- package/src/plugins/backup/multi-backup-driver.class.js +0 -1
- package/src/plugins/backup.plugin.js +7 -14
- package/src/plugins/concerns/plugin-dependencies.js +73 -19
- package/src/plugins/eventual-consistency/analytics.js +0 -2
- package/src/plugins/eventual-consistency/consolidation.js +2 -13
- package/src/plugins/eventual-consistency/index.js +0 -1
- package/src/plugins/eventual-consistency/install.js +1 -1
- package/src/plugins/geo.plugin.js +5 -6
- package/src/plugins/importer/index.js +1 -1
- package/src/plugins/plugin.class.js +5 -0
- package/src/plugins/relation.plugin.js +193 -57
- package/src/plugins/replicator.plugin.js +12 -21
- package/src/plugins/s3-queue.plugin.js +4 -4
- package/src/plugins/scheduler.plugin.js +10 -12
- package/src/plugins/state-machine.plugin.js +8 -12
- package/src/plugins/tfstate/README.md +1 -1
- package/src/plugins/tfstate/errors.js +3 -3
- package/src/plugins/tfstate/index.js +41 -67
- package/src/plugins/ttl.plugin.js +479 -304
- package/src/resource.class.js +263 -61
- package/src/schema.class.js +0 -2
- package/src/testing/factory.class.js +286 -0
- package/src/testing/index.js +15 -0
- package/src/testing/seeder.class.js +183 -0
- package/dist/s3db-cli.js +0 -55543
package/src/cli/index.js
CHANGED
|
@@ -330,52 +330,159 @@ program
|
|
|
330
330
|
}
|
|
331
331
|
});
|
|
332
332
|
|
|
333
|
-
//
|
|
333
|
+
// Console command (enhanced REPL)
|
|
334
|
+
program
|
|
335
|
+
.command('console')
|
|
336
|
+
.description('Enhanced interactive console')
|
|
337
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
338
|
+
.action(async (options) => {
|
|
339
|
+
await consoleREPL(options);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Interactive mode (alias for console)
|
|
334
343
|
program
|
|
335
344
|
.command('interactive')
|
|
336
|
-
.description('Interactive REPL mode')
|
|
345
|
+
.description('Interactive REPL mode (alias for console)')
|
|
337
346
|
.option('-c, --connection <string>', 'Connection string')
|
|
338
347
|
.action(async (options) => {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
348
|
+
await consoleREPL(options);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
async function consoleREPL(options) {
|
|
352
|
+
console.log(chalk.cyan.bold('\n┌─────────────────────────────────────┐'));
|
|
353
|
+
console.log(chalk.cyan.bold('│ S3DB Interactive Console v12.0 │'));
|
|
354
|
+
console.log(chalk.cyan.bold('└─────────────────────────────────────┘\n'));
|
|
355
|
+
|
|
356
|
+
const db = await getDatabase(options);
|
|
357
|
+
await db.init();
|
|
358
|
+
|
|
359
|
+
const resources = await db.listResources();
|
|
360
|
+
console.log(chalk.gray(`Connected to: ${db.client.config.bucket}`));
|
|
361
|
+
console.log(chalk.gray(`Resources: ${resources.length}\n`));
|
|
362
|
+
|
|
363
|
+
console.log(chalk.yellow('Quick commands:'));
|
|
364
|
+
console.log(chalk.gray(' .help - Show all commands'));
|
|
365
|
+
console.log(chalk.gray(' .resources - List all resources'));
|
|
366
|
+
console.log(chalk.gray(' .use <name> - Select a resource'));
|
|
367
|
+
console.log(chalk.gray(' .exit - Exit console\n'));
|
|
368
|
+
|
|
369
|
+
const repl = await import('repl');
|
|
370
|
+
const { Factory, Seeder } = await import('../testing/index.js');
|
|
371
|
+
|
|
372
|
+
// Set up global context
|
|
373
|
+
let currentResource = null;
|
|
374
|
+
|
|
375
|
+
const server = repl.start({
|
|
376
|
+
prompt: chalk.green('s3db> '),
|
|
377
|
+
useColors: true,
|
|
378
|
+
ignoreUndefined: true,
|
|
379
|
+
eval: async (cmd, context, filename, callback) => {
|
|
380
|
+
try {
|
|
381
|
+
const trimmed = cmd.trim().replace(/\n$/, '');
|
|
382
|
+
|
|
383
|
+
// Special commands
|
|
384
|
+
if (trimmed === '.help' || trimmed === 'help') {
|
|
385
|
+
console.log(chalk.cyan('\n📖 S3DB Console Commands:\n'));
|
|
386
|
+
console.log(chalk.bold('Database:'));
|
|
387
|
+
console.log(' db - Database instance');
|
|
388
|
+
console.log(' db.listResources() - List all resources');
|
|
389
|
+
console.log(' db.resource(name) - Get a resource');
|
|
390
|
+
console.log(chalk.bold('\nResource Selection:'));
|
|
391
|
+
console.log(' .use <name> - Select active resource');
|
|
392
|
+
console.log(' resource - Current resource (if selected)');
|
|
393
|
+
console.log(chalk.bold('\nData Operations:'));
|
|
394
|
+
console.log(' await resource.list() - List records');
|
|
395
|
+
console.log(' await resource.get(id) - Get record by ID');
|
|
396
|
+
console.log(' await resource.insert({})- Insert record');
|
|
397
|
+
console.log(' await resource.count() - Count records');
|
|
398
|
+
console.log(chalk.bold('\nTesting:'));
|
|
399
|
+
console.log(' Factory - Factory class');
|
|
400
|
+
console.log(' Seeder - Seeder class');
|
|
401
|
+
console.log(chalk.bold('\nUtilities:'));
|
|
402
|
+
console.log(' .resources - List resources');
|
|
403
|
+
console.log(' .clear - Clear console');
|
|
404
|
+
console.log(' .exit - Exit\n');
|
|
405
|
+
callback(null);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (trimmed === '.resources') {
|
|
410
|
+
const table = new Table({
|
|
411
|
+
head: ['Resource', 'Behavior', 'Partitions'],
|
|
412
|
+
style: { head: ['cyan'] }
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
resources.forEach(r => {
|
|
416
|
+
table.push([
|
|
417
|
+
r.name,
|
|
418
|
+
r.config.behavior || 'user-managed',
|
|
419
|
+
Object.keys(r.config.partitions || {}).length
|
|
420
|
+
]);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
console.log(table.toString());
|
|
424
|
+
callback(null);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (trimmed.startsWith('.use ')) {
|
|
429
|
+
const resourceName = trimmed.replace('.use ', '').trim();
|
|
430
|
+
try {
|
|
431
|
+
currentResource = await db.resource(resourceName);
|
|
432
|
+
console.log(chalk.green(`✓ Now using resource: ${resourceName}`));
|
|
433
|
+
context.resource = currentResource;
|
|
434
|
+
callback(null);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.log(chalk.red(`✗ Resource not found: ${resourceName}`));
|
|
364
437
|
callback(null);
|
|
365
|
-
} else {
|
|
366
|
-
// Default eval
|
|
367
|
-
const result = await eval(cmd);
|
|
368
|
-
callback(null, result);
|
|
369
438
|
}
|
|
370
|
-
|
|
371
|
-
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (trimmed === '.clear') {
|
|
443
|
+
console.clear();
|
|
444
|
+
callback(null);
|
|
445
|
+
return;
|
|
372
446
|
}
|
|
447
|
+
|
|
448
|
+
// Set up context
|
|
449
|
+
context.db = db;
|
|
450
|
+
context.resource = currentResource;
|
|
451
|
+
context.Factory = Factory;
|
|
452
|
+
context.Seeder = Seeder;
|
|
453
|
+
context.chalk = chalk;
|
|
454
|
+
context.Table = Table;
|
|
455
|
+
|
|
456
|
+
// Evaluate JavaScript
|
|
457
|
+
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
458
|
+
const fn = new AsyncFunction('context', `
|
|
459
|
+
with (context) {
|
|
460
|
+
return (async () => {
|
|
461
|
+
${trimmed}
|
|
462
|
+
})();
|
|
463
|
+
}
|
|
464
|
+
`);
|
|
465
|
+
|
|
466
|
+
const result = await fn(context);
|
|
467
|
+
callback(null, result);
|
|
468
|
+
|
|
469
|
+
} catch (error) {
|
|
470
|
+
console.log(chalk.red(`Error: ${error.message}`));
|
|
471
|
+
callback(null);
|
|
373
472
|
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
server.setupHistory(path.join(os.homedir(), '.s3db', 'history'), () => {});
|
|
473
|
+
}
|
|
377
474
|
});
|
|
378
475
|
|
|
476
|
+
// Set up history
|
|
477
|
+
server.setupHistory(path.join(os.homedir(), '.s3db', 'history'), () => {});
|
|
478
|
+
|
|
479
|
+
// Set up autocomplete
|
|
480
|
+
server.on('exit', () => {
|
|
481
|
+
console.log(chalk.cyan('\n👋 Bye!\n'));
|
|
482
|
+
process.exit(0);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
379
486
|
// Stats command
|
|
380
487
|
program
|
|
381
488
|
.command('stats [resource]')
|
|
@@ -383,37 +490,37 @@ program
|
|
|
383
490
|
.option('-c, --connection <string>', 'Connection string')
|
|
384
491
|
.action(async (resourceName, options) => {
|
|
385
492
|
const spinner = ora('Gathering stats...').start();
|
|
386
|
-
|
|
493
|
+
|
|
387
494
|
try {
|
|
388
495
|
const db = await getDatabase(options);
|
|
389
496
|
await db.init();
|
|
390
|
-
|
|
497
|
+
|
|
391
498
|
if (resourceName) {
|
|
392
499
|
const resource = await db.resource(resourceName);
|
|
393
500
|
const count = await resource.count();
|
|
394
501
|
spinner.stop();
|
|
395
|
-
|
|
502
|
+
|
|
396
503
|
console.log(chalk.cyan(`\nResource: ${resourceName}`));
|
|
397
504
|
console.log(`Total records: ${count}`);
|
|
398
505
|
} else {
|
|
399
506
|
const resources = await db.listResources();
|
|
400
507
|
spinner.stop();
|
|
401
|
-
|
|
508
|
+
|
|
402
509
|
console.log(chalk.cyan('\nDatabase Statistics'));
|
|
403
510
|
console.log(`Total resources: ${resources.length}`);
|
|
404
|
-
|
|
511
|
+
|
|
405
512
|
if (resources.length > 0) {
|
|
406
513
|
const table = new Table({
|
|
407
514
|
head: ['Resource', 'Count'],
|
|
408
515
|
style: { head: ['cyan'] }
|
|
409
516
|
});
|
|
410
|
-
|
|
517
|
+
|
|
411
518
|
for (const r of resources) {
|
|
412
519
|
const resource = await db.resource(r.name);
|
|
413
520
|
const count = await resource.count();
|
|
414
521
|
table.push([r.name, count]);
|
|
415
522
|
}
|
|
416
|
-
|
|
523
|
+
|
|
417
524
|
console.log(table.toString());
|
|
418
525
|
}
|
|
419
526
|
}
|
|
@@ -423,4 +530,808 @@ program
|
|
|
423
530
|
}
|
|
424
531
|
});
|
|
425
532
|
|
|
533
|
+
// Schema command
|
|
534
|
+
program
|
|
535
|
+
.command('schema <resource>')
|
|
536
|
+
.description('Show resource schema')
|
|
537
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
538
|
+
.option('-f, --format <type>', 'Output format: json, typescript, bigquery', 'json')
|
|
539
|
+
.action(async (resourceName, options) => {
|
|
540
|
+
const spinner = ora('Loading schema...').start();
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
const db = await getDatabase(options);
|
|
544
|
+
await db.init();
|
|
545
|
+
|
|
546
|
+
const resource = await db.resource(resourceName);
|
|
547
|
+
const schema = resource.export();
|
|
548
|
+
spinner.stop();
|
|
549
|
+
|
|
550
|
+
if (options.format === 'typescript') {
|
|
551
|
+
const { generateTypes } = await import('../concerns/typescript-generator.js');
|
|
552
|
+
const types = generateTypes(db);
|
|
553
|
+
console.log(types);
|
|
554
|
+
} else if (options.format === 'bigquery') {
|
|
555
|
+
console.log(chalk.cyan(`\nBigQuery DDL for ${resourceName}:\n`));
|
|
556
|
+
console.log(`CREATE TABLE \`project.dataset.${resourceName}\` (`);
|
|
557
|
+
|
|
558
|
+
const fields = [];
|
|
559
|
+
fields.push(' id STRING NOT NULL');
|
|
560
|
+
|
|
561
|
+
for (const [field, type] of Object.entries(schema.attributes)) {
|
|
562
|
+
const typeStr = type.toString();
|
|
563
|
+
let bqType = 'STRING';
|
|
564
|
+
|
|
565
|
+
if (typeStr.includes('number')) bqType = 'FLOAT64';
|
|
566
|
+
else if (typeStr.includes('boolean')) bqType = 'BOOL';
|
|
567
|
+
else if (typeStr.includes('array')) bqType = 'ARRAY<STRING>';
|
|
568
|
+
else if (typeStr.includes('object')) bqType = 'JSON';
|
|
569
|
+
else if (typeStr.includes('embedding')) bqType = 'ARRAY<FLOAT64>';
|
|
570
|
+
|
|
571
|
+
const required = typeStr.includes('required') ? 'NOT NULL' : '';
|
|
572
|
+
fields.push(` ${field} ${bqType} ${required}`.trim());
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (schema.timestamps) {
|
|
576
|
+
fields.push(' createdAt TIMESTAMP');
|
|
577
|
+
fields.push(' updatedAt TIMESTAMP');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
console.log(fields.join(',\n'));
|
|
581
|
+
console.log(');');
|
|
582
|
+
} else {
|
|
583
|
+
// JSON format (default)
|
|
584
|
+
console.log(JSON.stringify(schema, null, 2));
|
|
585
|
+
}
|
|
586
|
+
} catch (error) {
|
|
587
|
+
spinner.fail(chalk.red(error.message));
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Schema diff command
|
|
593
|
+
program
|
|
594
|
+
.command('schema-diff')
|
|
595
|
+
.description('Compare local schema files with deployed schemas')
|
|
596
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
597
|
+
.option('-d, --dir <path>', 'Local schema directory', './schemas')
|
|
598
|
+
.action(async (options) => {
|
|
599
|
+
const spinner = ora('Comparing schemas...').start();
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
const db = await getDatabase(options);
|
|
603
|
+
await db.init();
|
|
604
|
+
|
|
605
|
+
const remoteResources = await db.listResources();
|
|
606
|
+
spinner.stop();
|
|
607
|
+
|
|
608
|
+
// Try to load local schemas
|
|
609
|
+
let localSchemas = {};
|
|
610
|
+
try {
|
|
611
|
+
const schemaFiles = await fs.readdir(options.dir);
|
|
612
|
+
for (const file of schemaFiles) {
|
|
613
|
+
if (file.endsWith('.json')) {
|
|
614
|
+
const content = await fs.readFile(path.join(options.dir, file), 'utf-8');
|
|
615
|
+
const schema = JSON.parse(content);
|
|
616
|
+
localSchemas[schema.name] = schema;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
} catch (err) {
|
|
620
|
+
console.log(chalk.yellow(`No local schemas found in ${options.dir}`));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const table = new Table({
|
|
624
|
+
head: ['Resource', 'Status', 'Changes'],
|
|
625
|
+
style: { head: ['cyan'] }
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// Check remote resources
|
|
629
|
+
for (const remote of remoteResources) {
|
|
630
|
+
const local = localSchemas[remote.name];
|
|
631
|
+
|
|
632
|
+
if (!local) {
|
|
633
|
+
table.push([remote.name, chalk.yellow('Remote Only'), 'Not in local schemas']);
|
|
634
|
+
} else {
|
|
635
|
+
// Compare attributes
|
|
636
|
+
const remoteAttrs = Object.keys(remote.attributes || {}).sort();
|
|
637
|
+
const localAttrs = Object.keys(local.attributes || {}).sort();
|
|
638
|
+
|
|
639
|
+
if (JSON.stringify(remoteAttrs) !== JSON.stringify(localAttrs)) {
|
|
640
|
+
const diff = {
|
|
641
|
+
added: localAttrs.filter(a => !remoteAttrs.includes(a)),
|
|
642
|
+
removed: remoteAttrs.filter(a => !localAttrs.includes(a))
|
|
643
|
+
};
|
|
644
|
+
table.push([
|
|
645
|
+
remote.name,
|
|
646
|
+
chalk.yellow('Modified'),
|
|
647
|
+
`+${diff.added.length} -${diff.removed.length} fields`
|
|
648
|
+
]);
|
|
649
|
+
} else {
|
|
650
|
+
table.push([remote.name, chalk.green('Synced'), 'No changes']);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
delete localSchemas[remote.name];
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Check local-only schemas
|
|
658
|
+
for (const [name, schema] of Object.entries(localSchemas)) {
|
|
659
|
+
table.push([name, chalk.blue('Local Only'), 'Not deployed']);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
console.log(table.toString());
|
|
663
|
+
} catch (error) {
|
|
664
|
+
spinner.fail(chalk.red(error.message));
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// Count with aggregation
|
|
670
|
+
program
|
|
671
|
+
.command('count <resource>')
|
|
672
|
+
.description('Count records with optional grouping')
|
|
673
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
674
|
+
.option('-b, --by <field>', 'Group by field')
|
|
675
|
+
.option('-p, --partition <name>', 'Partition name')
|
|
676
|
+
.action(async (resourceName, options) => {
|
|
677
|
+
const spinner = ora('Counting...').start();
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
const db = await getDatabase(options);
|
|
681
|
+
await db.init();
|
|
682
|
+
|
|
683
|
+
const resource = await db.resource(resourceName);
|
|
684
|
+
|
|
685
|
+
if (options.by) {
|
|
686
|
+
// Group by aggregation
|
|
687
|
+
const listOptions = options.partition ? { partition: options.partition } : {};
|
|
688
|
+
const records = await resource.list(listOptions);
|
|
689
|
+
spinner.stop();
|
|
690
|
+
|
|
691
|
+
const grouped = {};
|
|
692
|
+
for (const record of records) {
|
|
693
|
+
const value = record[options.by] || '(null)';
|
|
694
|
+
grouped[value] = (grouped[value] || 0) + 1;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const table = new Table({
|
|
698
|
+
head: [options.by, 'Count'],
|
|
699
|
+
style: { head: ['cyan'] }
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
Object.entries(grouped)
|
|
703
|
+
.sort((a, b) => b[1] - a[1])
|
|
704
|
+
.forEach(([key, count]) => {
|
|
705
|
+
table.push([key, count]);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
console.log(table.toString());
|
|
709
|
+
console.log(chalk.gray(`\nTotal: ${records.length} records`));
|
|
710
|
+
} else {
|
|
711
|
+
const count = await resource.count();
|
|
712
|
+
spinner.stop();
|
|
713
|
+
console.log(chalk.cyan(`${resourceName}: ${count} records`));
|
|
714
|
+
}
|
|
715
|
+
} catch (error) {
|
|
716
|
+
spinner.fail(chalk.red(error.message));
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// Explain command
|
|
722
|
+
program
|
|
723
|
+
.command('explain <resource>')
|
|
724
|
+
.description('Show partition structure and query plans')
|
|
725
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
726
|
+
.option('-p, --partition <name>', 'Specific partition to explain')
|
|
727
|
+
.action(async (resourceName, options) => {
|
|
728
|
+
const spinner = ora('Analyzing...').start();
|
|
729
|
+
|
|
730
|
+
try {
|
|
731
|
+
const db = await getDatabase(options);
|
|
732
|
+
await db.init();
|
|
733
|
+
|
|
734
|
+
const resource = await db.resource(resourceName);
|
|
735
|
+
const schema = resource.export();
|
|
736
|
+
spinner.stop();
|
|
737
|
+
|
|
738
|
+
console.log(chalk.cyan(`\n📊 Resource: ${resourceName}\n`));
|
|
739
|
+
|
|
740
|
+
// Basic info
|
|
741
|
+
console.log(chalk.bold('Configuration:'));
|
|
742
|
+
console.log(` Behavior: ${schema.behavior || 'user-managed'}`);
|
|
743
|
+
console.log(` Timestamps: ${schema.timestamps ? '✓' : '✗'}`);
|
|
744
|
+
console.log(` Paranoid: ${schema.paranoid ? '✓' : '✗'}`);
|
|
745
|
+
console.log(` Async Partitions: ${schema.asyncPartitions ? '✓' : '✗'}`);
|
|
746
|
+
|
|
747
|
+
// Partitions
|
|
748
|
+
if (schema.partitions && Object.keys(schema.partitions).length > 0) {
|
|
749
|
+
console.log(chalk.bold('\nPartitions:'));
|
|
750
|
+
|
|
751
|
+
for (const [name, config] of Object.entries(schema.partitions)) {
|
|
752
|
+
if (options.partition && name !== options.partition) continue;
|
|
753
|
+
|
|
754
|
+
console.log(chalk.green(`\n ${name}:`));
|
|
755
|
+
console.log(` Fields: ${Object.keys(config.fields).join(', ')}`);
|
|
756
|
+
|
|
757
|
+
// Try to count partition keys
|
|
758
|
+
try {
|
|
759
|
+
const prefix = `resource=${resourceName}/partition=${name}/`;
|
|
760
|
+
const keys = await resource.client.listObjects({ prefix, maxKeys: 1000 });
|
|
761
|
+
|
|
762
|
+
// Extract unique partition values
|
|
763
|
+
const values = new Set();
|
|
764
|
+
keys.forEach(key => {
|
|
765
|
+
const parts = key.split('/');
|
|
766
|
+
const valueParts = parts.filter(p => !p.startsWith('resource=') && !p.startsWith('partition=') && !p.startsWith('id='));
|
|
767
|
+
valueParts.forEach(v => values.add(v));
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
console.log(` Unique values: ${values.size}`);
|
|
771
|
+
console.log(` Total keys: ${keys.length}`);
|
|
772
|
+
console.log(` Key pattern: ${prefix}<values>/id=<id>`);
|
|
773
|
+
} catch (err) {
|
|
774
|
+
console.log(chalk.gray(' (Unable to analyze partition keys)'));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
} else {
|
|
778
|
+
console.log(chalk.yellow('\nNo partitions configured'));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Query plan
|
|
782
|
+
console.log(chalk.bold('\n🔍 Query Optimization:'));
|
|
783
|
+
if (schema.partitions && Object.keys(schema.partitions).length > 0) {
|
|
784
|
+
console.log(chalk.green(' ✓ O(1) partition lookups available'));
|
|
785
|
+
console.log(' 💡 Use getFromPartition() for best performance');
|
|
786
|
+
} else {
|
|
787
|
+
console.log(chalk.yellow(' ⚠️ O(n) full scans only (no partitions)'));
|
|
788
|
+
console.log(' 💡 Consider adding partitions for common queries');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
} catch (error) {
|
|
792
|
+
spinner.fail(chalk.red(error.message));
|
|
793
|
+
process.exit(1);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// Analyze command
|
|
798
|
+
program
|
|
799
|
+
.command('analyze <resource>')
|
|
800
|
+
.description('Analyze resource performance and storage')
|
|
801
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
802
|
+
.action(async (resourceName, options) => {
|
|
803
|
+
const spinner = ora('Analyzing resource...').start();
|
|
804
|
+
|
|
805
|
+
try {
|
|
806
|
+
const db = await getDatabase(options);
|
|
807
|
+
await db.init();
|
|
808
|
+
|
|
809
|
+
const resource = await db.resource(resourceName);
|
|
810
|
+
|
|
811
|
+
// Get sample records
|
|
812
|
+
const sample = await resource.list({ limit: 100 });
|
|
813
|
+
const count = await resource.count();
|
|
814
|
+
spinner.stop();
|
|
815
|
+
|
|
816
|
+
console.log(chalk.cyan(`\n📈 Analysis: ${resourceName}\n`));
|
|
817
|
+
|
|
818
|
+
// Record count
|
|
819
|
+
console.log(chalk.bold('Records:'));
|
|
820
|
+
console.log(` Total: ${count}`);
|
|
821
|
+
console.log(` Sampled: ${sample.length}`);
|
|
822
|
+
|
|
823
|
+
// Size analysis
|
|
824
|
+
if (sample.length > 0) {
|
|
825
|
+
console.log(chalk.bold('\nSize Analysis (from sample):'));
|
|
826
|
+
|
|
827
|
+
const sizes = sample.map(r => {
|
|
828
|
+
const str = JSON.stringify(r);
|
|
829
|
+
return Buffer.byteLength(str, 'utf8');
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
const avg = sizes.reduce((a, b) => a + b, 0) / sizes.length;
|
|
833
|
+
const min = Math.min(...sizes);
|
|
834
|
+
const max = Math.max(...sizes);
|
|
835
|
+
|
|
836
|
+
console.log(` Average: ${(avg / 1024).toFixed(2)} KB`);
|
|
837
|
+
console.log(` Min: ${(min / 1024).toFixed(2)} KB`);
|
|
838
|
+
console.log(` Max: ${(max / 1024).toFixed(2)} KB`);
|
|
839
|
+
console.log(` Estimated total: ${((avg * count) / 1024 / 1024).toFixed(2)} MB`);
|
|
840
|
+
|
|
841
|
+
// Field analysis
|
|
842
|
+
console.log(chalk.bold('\nField Usage:'));
|
|
843
|
+
const fieldCounts = {};
|
|
844
|
+
const fieldSizes = {};
|
|
845
|
+
|
|
846
|
+
sample.forEach(record => {
|
|
847
|
+
Object.keys(record).forEach(field => {
|
|
848
|
+
fieldCounts[field] = (fieldCounts[field] || 0) + 1;
|
|
849
|
+
const value = record[field];
|
|
850
|
+
if (value !== null && value !== undefined) {
|
|
851
|
+
const size = Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
852
|
+
fieldSizes[field] = (fieldSizes[field] || 0) + size;
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
const table = new Table({
|
|
858
|
+
head: ['Field', 'Fill Rate', 'Avg Size'],
|
|
859
|
+
style: { head: ['cyan'] }
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
Object.keys(fieldCounts).forEach(field => {
|
|
863
|
+
const fillRate = ((fieldCounts[field] / sample.length) * 100).toFixed(1);
|
|
864
|
+
const avgSize = fieldSizes[field] ? (fieldSizes[field] / fieldCounts[field]) : 0;
|
|
865
|
+
table.push([
|
|
866
|
+
field,
|
|
867
|
+
`${fillRate}%`,
|
|
868
|
+
`${avgSize.toFixed(0)} bytes`
|
|
869
|
+
]);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
console.log(table.toString());
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Performance recommendations
|
|
876
|
+
console.log(chalk.bold('\n💡 Recommendations:'));
|
|
877
|
+
|
|
878
|
+
const schema = resource.export();
|
|
879
|
+
if (!schema.partitions || Object.keys(schema.partitions).length === 0) {
|
|
880
|
+
console.log(chalk.yellow(' • Add partitions for frequently queried fields'));
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (count > 1000 && !schema.asyncPartitions) {
|
|
884
|
+
console.log(chalk.yellow(' • Enable asyncPartitions for faster writes'));
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (sample.length > 0) {
|
|
888
|
+
const avgSize = sample.reduce((sum, r) => {
|
|
889
|
+
return sum + Buffer.byteLength(JSON.stringify(r), 'utf8');
|
|
890
|
+
}, 0) / sample.length;
|
|
891
|
+
|
|
892
|
+
if (avgSize > 2000) {
|
|
893
|
+
console.log(chalk.yellow(' • Consider body-only behavior for large records'));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
} catch (error) {
|
|
898
|
+
spinner.fail(chalk.red(error.message));
|
|
899
|
+
process.exit(1);
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// Test commands
|
|
904
|
+
const test = program.command('test').description('Testing utilities');
|
|
905
|
+
|
|
906
|
+
test
|
|
907
|
+
.command('seed [resource]')
|
|
908
|
+
.description('Seed database with test data')
|
|
909
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
910
|
+
.option('-n, --count <number>', 'Number of records to create', '10')
|
|
911
|
+
.option('-f, --file <path>', 'Seed from factory definition file')
|
|
912
|
+
.action(async (resourceName, options) => {
|
|
913
|
+
const spinner = ora('Seeding database...').start();
|
|
914
|
+
|
|
915
|
+
try {
|
|
916
|
+
const db = await getDatabase(options);
|
|
917
|
+
await db.init();
|
|
918
|
+
|
|
919
|
+
const { Factory, Seeder } = await import('../testing/index.js');
|
|
920
|
+
Factory.setDatabase(db);
|
|
921
|
+
|
|
922
|
+
const seeder = new Seeder(db, { verbose: false });
|
|
923
|
+
|
|
924
|
+
if (options.file) {
|
|
925
|
+
// Load factory definitions from file
|
|
926
|
+
const factoryModule = await import(path.resolve(options.file));
|
|
927
|
+
spinner.text = 'Running custom seed...';
|
|
928
|
+
|
|
929
|
+
const result = await seeder.call(factoryModule.default || factoryModule.seed);
|
|
930
|
+
spinner.succeed(chalk.green('✓ Custom seed completed'));
|
|
931
|
+
|
|
932
|
+
console.log(JSON.stringify(result, null, 2));
|
|
933
|
+
} else if (resourceName) {
|
|
934
|
+
// Seed specific resource
|
|
935
|
+
const count = parseInt(options.count);
|
|
936
|
+
spinner.text = `Seeding ${count} ${resourceName}...`;
|
|
937
|
+
|
|
938
|
+
const factory = Factory.get(resourceName);
|
|
939
|
+
if (!factory) {
|
|
940
|
+
spinner.fail(chalk.red(`No factory found for '${resourceName}'`));
|
|
941
|
+
console.log(chalk.yellow('\n💡 Define a factory first or use --file option'));
|
|
942
|
+
process.exit(1);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const records = await factory.createMany(count);
|
|
946
|
+
spinner.succeed(chalk.green(`✓ Created ${records.length} ${resourceName}`));
|
|
947
|
+
|
|
948
|
+
console.log(chalk.gray(`IDs: ${records.map(r => r.id).slice(0, 5).join(', ')}${records.length > 5 ? '...' : ''}`));
|
|
949
|
+
} else {
|
|
950
|
+
// Seed all resources using factories
|
|
951
|
+
const resources = await db.listResources();
|
|
952
|
+
spinner.stop();
|
|
953
|
+
|
|
954
|
+
const specs = {};
|
|
955
|
+
for (const r of resources) {
|
|
956
|
+
const factory = Factory.get(r.name);
|
|
957
|
+
if (factory) {
|
|
958
|
+
specs[r.name] = parseInt(options.count);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (Object.keys(specs).length === 0) {
|
|
963
|
+
console.log(chalk.yellow('No factories defined. Use --file to load factory definitions.'));
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
console.log(chalk.cyan('Seeding with factories:'));
|
|
968
|
+
console.log(specs);
|
|
969
|
+
|
|
970
|
+
const created = await seeder.seed(specs);
|
|
971
|
+
|
|
972
|
+
console.log(chalk.green('\n✓ Seed completed:'));
|
|
973
|
+
Object.entries(created).forEach(([name, records]) => {
|
|
974
|
+
console.log(` ${name}: ${records.length} records`);
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
} catch (error) {
|
|
978
|
+
spinner.fail(chalk.red(error.message));
|
|
979
|
+
console.error(error.stack);
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
test
|
|
985
|
+
.command('setup')
|
|
986
|
+
.description('Setup isolated test database')
|
|
987
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
988
|
+
.option('-n, --name <name>', 'Test database name', `test-${Date.now()}`)
|
|
989
|
+
.option('-f, --fixtures <path>', 'Load fixtures from file')
|
|
990
|
+
.action(async (options) => {
|
|
991
|
+
const spinner = ora('Setting up test database...').start();
|
|
992
|
+
|
|
993
|
+
try {
|
|
994
|
+
// Create test database connection
|
|
995
|
+
const config = await loadConfig();
|
|
996
|
+
let baseConnection = options.connection || config.connection || process.env.S3DB_CONNECTION;
|
|
997
|
+
|
|
998
|
+
if (!baseConnection) {
|
|
999
|
+
spinner.fail(chalk.red('No connection string provided'));
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Modify connection to use test database path
|
|
1004
|
+
const url = new URL(baseConnection);
|
|
1005
|
+
const originalPath = url.pathname;
|
|
1006
|
+
url.pathname = `${originalPath}/${options.name}`;
|
|
1007
|
+
const testConnection = url.toString();
|
|
1008
|
+
|
|
1009
|
+
const db = new S3db({ connectionString: testConnection });
|
|
1010
|
+
await db.init();
|
|
1011
|
+
|
|
1012
|
+
spinner.text = 'Loading fixtures...';
|
|
1013
|
+
|
|
1014
|
+
if (options.fixtures) {
|
|
1015
|
+
const fixturesModule = await import(path.resolve(options.fixtures));
|
|
1016
|
+
const fixtures = fixturesModule.default || fixturesModule.fixtures;
|
|
1017
|
+
|
|
1018
|
+
if (typeof fixtures === 'function') {
|
|
1019
|
+
await fixtures(db);
|
|
1020
|
+
} else {
|
|
1021
|
+
// Load fixtures as data
|
|
1022
|
+
for (const [resourceName, records] of Object.entries(fixtures)) {
|
|
1023
|
+
const resource = await db.resource(resourceName);
|
|
1024
|
+
for (const record of records) {
|
|
1025
|
+
await resource.insert(record);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
spinner.succeed(chalk.green('✓ Test database ready'));
|
|
1032
|
+
|
|
1033
|
+
console.log(chalk.cyan('\nTest Database:'));
|
|
1034
|
+
console.log(` Name: ${options.name}`);
|
|
1035
|
+
console.log(` Connection: ${testConnection}`);
|
|
1036
|
+
console.log(chalk.gray('\n💡 Use this connection string for your tests'));
|
|
1037
|
+
console.log(chalk.gray(`💡 Teardown with: s3db test teardown --name ${options.name}`));
|
|
1038
|
+
|
|
1039
|
+
// Save test connection for teardown
|
|
1040
|
+
const testConfig = { ...config, testConnection, testName: options.name };
|
|
1041
|
+
await saveConfig(testConfig);
|
|
1042
|
+
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
spinner.fail(chalk.red(error.message));
|
|
1045
|
+
console.error(error.stack);
|
|
1046
|
+
process.exit(1);
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
test
|
|
1051
|
+
.command('teardown')
|
|
1052
|
+
.description('Clean up test database')
|
|
1053
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
1054
|
+
.option('-n, --name <name>', 'Test database name')
|
|
1055
|
+
.option('--all', 'Teardown all test databases')
|
|
1056
|
+
.action(async (options) => {
|
|
1057
|
+
const spinner = ora('Cleaning up...').start();
|
|
1058
|
+
|
|
1059
|
+
try {
|
|
1060
|
+
const config = await loadConfig();
|
|
1061
|
+
let connection = options.connection;
|
|
1062
|
+
|
|
1063
|
+
if (!connection && options.name) {
|
|
1064
|
+
const baseConnection = config.connection || process.env.S3DB_CONNECTION;
|
|
1065
|
+
const url = new URL(baseConnection);
|
|
1066
|
+
const originalPath = url.pathname.replace(/\/[^\/]+$/, '');
|
|
1067
|
+
url.pathname = `${originalPath}/${options.name}`;
|
|
1068
|
+
connection = url.toString();
|
|
1069
|
+
} else if (!connection && config.testConnection) {
|
|
1070
|
+
connection = config.testConnection;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if (!connection) {
|
|
1074
|
+
spinner.fail(chalk.red('No test database to teardown'));
|
|
1075
|
+
process.exit(1);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const db = new S3db({ connectionString: connection });
|
|
1079
|
+
await db.init();
|
|
1080
|
+
|
|
1081
|
+
const { Seeder } = await import('../testing/index.js');
|
|
1082
|
+
const seeder = new Seeder(db, { verbose: false });
|
|
1083
|
+
|
|
1084
|
+
spinner.text = 'Resetting database...';
|
|
1085
|
+
await seeder.reset();
|
|
1086
|
+
|
|
1087
|
+
spinner.succeed(chalk.green('✓ Test database cleaned up'));
|
|
1088
|
+
|
|
1089
|
+
// Clean up config
|
|
1090
|
+
if (config.testConnection) {
|
|
1091
|
+
delete config.testConnection;
|
|
1092
|
+
delete config.testName;
|
|
1093
|
+
await saveConfig(config);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
spinner.fail(chalk.red(error.message));
|
|
1098
|
+
console.error(error.stack);
|
|
1099
|
+
process.exit(1);
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
test
|
|
1104
|
+
.command('truncate <resource>')
|
|
1105
|
+
.description('Delete all data from a resource')
|
|
1106
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
1107
|
+
.option('--force', 'Skip confirmation')
|
|
1108
|
+
.action(async (resourceName, options) => {
|
|
1109
|
+
if (!options.force) {
|
|
1110
|
+
const { confirm } = await inquirer.prompt([
|
|
1111
|
+
{
|
|
1112
|
+
type: 'confirm',
|
|
1113
|
+
name: 'confirm',
|
|
1114
|
+
message: `This will delete ALL data from ${resourceName}. Continue?`,
|
|
1115
|
+
default: false
|
|
1116
|
+
}
|
|
1117
|
+
]);
|
|
1118
|
+
|
|
1119
|
+
if (!confirm) {
|
|
1120
|
+
console.log(chalk.yellow('Cancelled'));
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const spinner = ora(`Truncating ${resourceName}...`).start();
|
|
1126
|
+
|
|
1127
|
+
try {
|
|
1128
|
+
const db = await getDatabase(options);
|
|
1129
|
+
await db.init();
|
|
1130
|
+
|
|
1131
|
+
const { Seeder } = await import('../testing/index.js');
|
|
1132
|
+
const seeder = new Seeder(db, { verbose: false });
|
|
1133
|
+
|
|
1134
|
+
await seeder.truncate([resourceName]);
|
|
1135
|
+
spinner.succeed(chalk.green(`✓ ${resourceName} truncated`));
|
|
1136
|
+
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
spinner.fail(chalk.red(error.message));
|
|
1139
|
+
process.exit(1);
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
// Migration commands
|
|
1144
|
+
const migrate = program.command('migrate').description('Database migrations');
|
|
1145
|
+
|
|
1146
|
+
migrate
|
|
1147
|
+
.command('generate <name>')
|
|
1148
|
+
.description('Generate a new migration file')
|
|
1149
|
+
.option('-d, --dir <path>', 'Migrations directory', './migrations')
|
|
1150
|
+
.action(async (name, options) => {
|
|
1151
|
+
const spinner = ora('Generating migration...').start();
|
|
1152
|
+
|
|
1153
|
+
try {
|
|
1154
|
+
const { MigrationManager } = await import('./migration-manager.js');
|
|
1155
|
+
const manager = new MigrationManager(null, options.dir);
|
|
1156
|
+
|
|
1157
|
+
const { filename, filepath } = await manager.generate(name);
|
|
1158
|
+
spinner.succeed(chalk.green('✓ Migration generated'));
|
|
1159
|
+
|
|
1160
|
+
console.log(chalk.cyan('\nMigration file:'));
|
|
1161
|
+
console.log(` ${filepath}`);
|
|
1162
|
+
console.log(chalk.gray('\n💡 Edit the file to add your migration logic'));
|
|
1163
|
+
console.log(chalk.gray('💡 Run with: s3db migrate up'));
|
|
1164
|
+
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
spinner.fail(chalk.red(error.message));
|
|
1167
|
+
console.error(error.stack);
|
|
1168
|
+
process.exit(1);
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
migrate
|
|
1173
|
+
.command('up')
|
|
1174
|
+
.description('Run pending migrations')
|
|
1175
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
1176
|
+
.option('-d, --dir <path>', 'Migrations directory', './migrations')
|
|
1177
|
+
.option('-s, --step <number>', 'Number of migrations to run')
|
|
1178
|
+
.action(async (options) => {
|
|
1179
|
+
const spinner = ora('Running migrations...').start();
|
|
1180
|
+
|
|
1181
|
+
try {
|
|
1182
|
+
const db = await getDatabase(options);
|
|
1183
|
+
await db.init();
|
|
1184
|
+
|
|
1185
|
+
const { MigrationManager } = await import('./migration-manager.js');
|
|
1186
|
+
const manager = new MigrationManager(db, options.dir);
|
|
1187
|
+
await manager.init();
|
|
1188
|
+
|
|
1189
|
+
const step = options.step ? parseInt(options.step) : null;
|
|
1190
|
+
const result = await manager.up({ step });
|
|
1191
|
+
|
|
1192
|
+
spinner.succeed(chalk.green(`✓ ${result.message}`));
|
|
1193
|
+
|
|
1194
|
+
if (result.migrations.length > 0) {
|
|
1195
|
+
console.log(chalk.cyan('\nMigrations executed:'));
|
|
1196
|
+
result.migrations.forEach(m => console.log(` • ${m}`));
|
|
1197
|
+
console.log(chalk.gray(`\nBatch: ${result.batch}`));
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
spinner.fail(chalk.red(error.message));
|
|
1202
|
+
console.error(error.stack);
|
|
1203
|
+
process.exit(1);
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
migrate
|
|
1208
|
+
.command('down')
|
|
1209
|
+
.description('Rollback migrations')
|
|
1210
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
1211
|
+
.option('-d, --dir <path>', 'Migrations directory', './migrations')
|
|
1212
|
+
.option('-s, --step <number>', 'Number of migrations to rollback', '1')
|
|
1213
|
+
.action(async (options) => {
|
|
1214
|
+
const spinner = ora('Rolling back migrations...').start();
|
|
1215
|
+
|
|
1216
|
+
try {
|
|
1217
|
+
const db = await getDatabase(options);
|
|
1218
|
+
await db.init();
|
|
1219
|
+
|
|
1220
|
+
const { MigrationManager } = await import('./migration-manager.js');
|
|
1221
|
+
const manager = new MigrationManager(db, options.dir);
|
|
1222
|
+
await manager.init();
|
|
1223
|
+
|
|
1224
|
+
const step = parseInt(options.step);
|
|
1225
|
+
const result = await manager.down({ step });
|
|
1226
|
+
|
|
1227
|
+
spinner.succeed(chalk.green(`✓ ${result.message}`));
|
|
1228
|
+
|
|
1229
|
+
if (result.migrations.length > 0) {
|
|
1230
|
+
console.log(chalk.cyan('\nMigrations rolled back:'));
|
|
1231
|
+
result.migrations.forEach(m => console.log(` • ${m}`));
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
spinner.fail(chalk.red(error.message));
|
|
1236
|
+
console.error(error.stack);
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
migrate
|
|
1242
|
+
.command('reset')
|
|
1243
|
+
.description('Reset all migrations')
|
|
1244
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
1245
|
+
.option('-d, --dir <path>', 'Migrations directory', './migrations')
|
|
1246
|
+
.option('--force', 'Skip confirmation')
|
|
1247
|
+
.action(async (options) => {
|
|
1248
|
+
if (!options.force) {
|
|
1249
|
+
const { confirm } = await inquirer.prompt([
|
|
1250
|
+
{
|
|
1251
|
+
type: 'confirm',
|
|
1252
|
+
name: 'confirm',
|
|
1253
|
+
message: 'This will rollback ALL migrations. Continue?',
|
|
1254
|
+
default: false
|
|
1255
|
+
}
|
|
1256
|
+
]);
|
|
1257
|
+
|
|
1258
|
+
if (!confirm) {
|
|
1259
|
+
console.log(chalk.yellow('Cancelled'));
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const spinner = ora('Resetting migrations...').start();
|
|
1265
|
+
|
|
1266
|
+
try {
|
|
1267
|
+
const db = await getDatabase(options);
|
|
1268
|
+
await db.init();
|
|
1269
|
+
|
|
1270
|
+
const { MigrationManager } = await import('./migration-manager.js');
|
|
1271
|
+
const manager = new MigrationManager(db, options.dir);
|
|
1272
|
+
await manager.init();
|
|
1273
|
+
|
|
1274
|
+
const result = await manager.reset();
|
|
1275
|
+
spinner.succeed(chalk.green(`✓ ${result.message}`));
|
|
1276
|
+
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
spinner.fail(chalk.red(error.message));
|
|
1279
|
+
console.error(error.stack);
|
|
1280
|
+
process.exit(1);
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
migrate
|
|
1285
|
+
.command('status')
|
|
1286
|
+
.description('Show migration status')
|
|
1287
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
1288
|
+
.option('-d, --dir <path>', 'Migrations directory', './migrations')
|
|
1289
|
+
.action(async (options) => {
|
|
1290
|
+
const spinner = ora('Checking migration status...').start();
|
|
1291
|
+
|
|
1292
|
+
try {
|
|
1293
|
+
const db = await getDatabase(options);
|
|
1294
|
+
await db.init();
|
|
1295
|
+
|
|
1296
|
+
const { MigrationManager } = await import('./migration-manager.js');
|
|
1297
|
+
const manager = new MigrationManager(db, options.dir);
|
|
1298
|
+
await manager.init();
|
|
1299
|
+
|
|
1300
|
+
const status = await manager.status();
|
|
1301
|
+
spinner.stop();
|
|
1302
|
+
|
|
1303
|
+
if (status.length === 0) {
|
|
1304
|
+
console.log(chalk.yellow('No migrations found'));
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const table = new Table({
|
|
1309
|
+
head: ['Migration', 'Status', 'Batch', 'Executed At'],
|
|
1310
|
+
style: { head: ['cyan'] }
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
status.forEach(m => {
|
|
1314
|
+
const statusColor = m.status === 'executed' ? chalk.green : chalk.yellow;
|
|
1315
|
+
table.push([
|
|
1316
|
+
m.name,
|
|
1317
|
+
statusColor(m.status),
|
|
1318
|
+
m.batch || '-',
|
|
1319
|
+
m.executedAt ? new Date(m.executedAt).toLocaleString() : '-'
|
|
1320
|
+
]);
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
console.log(table.toString());
|
|
1324
|
+
|
|
1325
|
+
const pending = status.filter(m => m.status === 'pending').length;
|
|
1326
|
+
const executed = status.filter(m => m.status === 'executed').length;
|
|
1327
|
+
|
|
1328
|
+
console.log(chalk.gray(`\nTotal: ${status.length} | Executed: ${executed} | Pending: ${pending}`));
|
|
1329
|
+
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
spinner.fail(chalk.red(error.message));
|
|
1332
|
+
console.error(error.stack);
|
|
1333
|
+
process.exit(1);
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
|
|
426
1337
|
program.parse(process.argv);
|