flap/directives/FlapDirective.js

/**
 * @module flitter-flap/FlapDirective
 */

const Directive = require('flitter-cli/Directive')
const path = require('path')
const flapper = require('../FlapHelper')(false)
const migrate = require('node-migration')

/**
 * ./flitter directive for interacting with flitter-flap
 * 
 * @extends module:flitter-cli/Directive~Directive
 */
class FlapDirective extends Directive {
    /**
     * Defines the services required by this directive.
     * @returns {Array<string>}
     */
    static get services() {
        return [...super.services, 'utility', 'flap']
    }

    /**
     * Get the name of the command for this directive. This is what is used by ./flitter.
     * 
     * @static
     * @returns {string} "flap"
     */
    static name(){
        return "flap"
    }

    /**
     * Get the options provided by this directive. False to disable parsing.
     * @returns {boolean}
     */
    static options() {
        return false
    }

    /**
     * Get the usage text for this directive.
     * 
     * @static
     * @returns {string}
     */
    static help(){
        return "Flitter Flap -- updates Flitter in-place (experimental)"
    }

    /**
     * Get the ASCII-text flap logo.
     * 
     * @returns {string}
     */
    logo(){
        return `
\\         \\  \\
 -  flap   -  -
/         /  /
`
    }

    /**
     * Get the usage information string for flap.
     * 
     * @returns {string}
     */
    usage(){
        // it may look weird, but the ascii art is actually not broken
        return `
 __      __ _              __    __   
 \\ \\    / _| |             \\ \\   \\ \\  
  \\ \\  | |_| | __ _ _ __    \\ \\   \\ \\ 
   > > |  _| |/ _\` | '_ \\    > >   > >
  / /  | | | | (_| | |_) |  / /   / / 
 /_/   |_| |_|\\__,_| .__/  /_/   /_/  
                   | |                
                   |_|                

Welcome to Flitter Flap! Flap is a tool designed to make it
easy to upgrade Flitter in-place. It uses a migration-based
system to modify the parts of Flitter that aren't a part of
yarn packages.

USAGE:  ./flitter flap <directive> [OPTIONS]

        help        :  display usage info
        do          :  run all pending migrations
        do <name>   :  run pending migrations from unit with name <name>
        undo        :  undo applied migrations from all units
        undo <name> :  undo applied migrations from unit with name <name>
        stamp       :  get the current UNIX timestamp (useful for creating migrations)
        
        OPTIONS:
        
        --dry       :  don't change any files, just list changes to be made
`
    }

    /**
     * Apply migrations for the target unit in the specified direction. This uses the the migration tracking file
     * specified in {@link module:flitter-flap/FlapUnit~FlapUnit#file}. If dry mode is specified, then a temporary copy of this file will be created and
     * deleted, and the migrations will be provided a set of FlapHelper tools with dry mode enabled.
     * 
     * @param {module:libflitter/app/FlitterApp~FlitterApp} app - the Flitter app
     * @param {string} target - name of the unit from which migrations should be applied. This unit's migration directory is looked up from the list created in {@link module:flitter-flap/FlapUnit~FlapUnit#go}.
     * @param {boolean} dry - If true, then the FlapHelper functions provided to the migrations will be run in dry mode. This specifies that files should not be modified.
     * @param {"up" | "down"} direction - Direction in which migrations should be applied.
     * @returns {Promise<void>}
     */
    async do_migration(app, target, dry, direction="up"){
        // resolve the folder containing the migration files
        const folder = path.resolve(this.flap.loaded_migrations[target])
        this.info("\n\nWill migrate target: "+target)

        // create the setup folder with the toolchain
        const setup_contents = `const FH = require('flitter-flap/FlapHelper')(${dry})
module.exports = (ctx) => { ctx.flap = FH }`
        await flapper.write_file(path.resolve(folder, 'setup.js'), setup_contents)

        // the file tracking applied migrations
        const file_root = path.resolve(this.flap.dir, target)
        let file = file_root+'.json'
        
        if ( !await flapper.exists(file) ){
            await flapper.write_file(file, '[]')
        }

        // if we're in dry-run mode, create a copy of the tracking file
        if ( dry ){
            await flapper.touch(file)
            file = file_root+".dry.json"
            await flapper.copy(file_root+'.json', file)
        }

        // do migration
        await migrate.run(direction, { dir: folder, file })

        // if we're in dry run mode, delete the copy of the tracking file
        if ( dry ){
            await flapper.delete(file)
        }

        // delete the setup file from the migrations folder
        await flapper.delete(path.resolve(folder, 'setup.js'))
    }

    /**
     * Handle an invocation of this command. Interprets the CLI arguments and handles them accordingly.
     * @param {module:libflitter/app/FlitterApp~FlitterApp} app - the Flitter app
     * @param {Object} argv - command line arguments passed in from ./flitter
     * @returns {Promise<void>}
     */
    async handle(app, argv){        
        const dry = argv.includes("--dry")
        argv = argv.filter((val, ind, arr) => { return ( val !== "--dry" ) })
        
        if ( dry ) console.log("Running in dry-run mode. Flap helpers won't modify files.")
        
        switch (argv[0]){
            case "do":
                console.log(this.logo())
                if ( argv[1] ){
                    if ( argv[1] in this.flap.loaded_migrations ){
                        await this.do_migration(app, argv[1], dry)
                        
                    }
                    else {
                        console.log("ERROR: Could not find unit to migrate with name: "+argv[1])
                    }
                }
                else {
                    this.info("Will migrate all.", 2)
                    
                    for ( let target in this.flap.loaded_migrations ){
                        
                        await this.do_migration(app, target, dry)
                        
                    }
                    
                }
                
                break
            case "undo":
                console.log(this.logo())
                if ( argv[1] ){
                    if ( argv[1] in this.flap.loaded_migrations ){
                        await this.do_migration(app, argv[1], dry, 'down')
                    }
                    else {
                        console.log("ERROR: Could not find unit to migrate with name: "+argv[1])
                    }
                }
                else {
                    for ( let target in this.flap.loaded_migrations ){
                        await this.do_migration(app, target, dry, 'down')
                    }
                }
                
                break
            case "stamp":
                console.log("The current UNIX time is: "+Math.floor(new Date() / 1000))
                break
            case "help":
                console.log(this.usage())
                break
            default:
                console.log("Invalid operation.")
                console.log(this.usage())
        }
    }
    
}

module.exports = exports = FlapDirective