├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── foxx ├── fixtures ├── echo-script.zip ├── minimal-working-service.js ├── minimal-working-service.zip ├── minimal-working-service │ ├── index.js │ └── manifest.json ├── minimal-working-setup-teardown.zip ├── passwordFile ├── service-service-service.zip ├── sloppy-service │ └── no-manifest.json ├── symlink-service │ ├── manifest.json │ ├── recursion │ └── symlink-folder ├── with-configuration.zip ├── with-dependencies.zip ├── with-readme.zip └── with-tests.zip ├── lib ├── bundle.js ├── cmds │ ├── add.js │ ├── add │ │ ├── crud.js │ │ ├── router.js │ │ ├── script.js │ │ └── test.js │ ├── bundle.js │ ├── config.js │ ├── deps.js │ ├── download.js │ ├── ignore.js │ ├── init.js │ ├── install.js │ ├── list.js │ ├── replace.js │ ├── run.js │ ├── scripts.js │ ├── server.js │ ├── server │ │ ├── list.js │ │ ├── remove.js │ │ ├── set.js │ │ └── show.js │ ├── set-dev.js │ ├── set-prod.js │ ├── show.js │ ├── test.js │ ├── uninstall.js │ └── upgrade.js ├── errors.js ├── generator │ ├── index.js │ └── wizard.js ├── ignore.js ├── index.js ├── ini.js ├── main.js ├── reporters.js ├── resolveServer.js ├── resolveToStream.js ├── test │ ├── add-crud.js │ ├── add-router.js │ ├── add-script.js │ ├── add-test.js │ ├── bundle.js │ ├── config.js │ ├── deps.js │ ├── download.js │ ├── errors.js │ ├── fs │ │ └── index.js │ ├── helper │ │ └── index.js │ ├── ignore.js │ ├── init.js │ ├── install.js │ ├── list.js │ ├── replace.js │ ├── run.js │ ├── scripts.js │ ├── server-list.js │ ├── server-remove.js │ ├── server-set.js │ ├── server-show.js │ ├── server.js │ ├── set-dev.js │ ├── set-prod.js │ ├── show.js │ ├── test.js │ ├── uninstall.js │ ├── upgrade.js │ └── util │ │ └── index.js └── util │ ├── array.js │ ├── cli.js │ ├── client.js │ ├── fs.js │ ├── log.js │ ├── parseOptions.js │ ├── streamToBuffer.js │ ├── text.js │ └── zip.js ├── package.json └── templates ├── LICENSE.ejs ├── README.md.ejs ├── crud.js.ejs ├── example └── index.js.ejs ├── index.js.ejs ├── router.js.ejs ├── script.js.ejs ├── setup.js.ejs ├── teardown.js.ejs └── test.js.ejs /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "extends": ["eslint:recommended", "prettier"], 7 | "parser": "babel-eslint", 8 | "rules": { 9 | "no-console": "warn" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | yarn.lock 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [2.1.1] - 2022-11-27 9 | 10 | ### Fixed 11 | 12 | - Fixed `TypeError: db.useDatabase is not a function` ([#50](https://github.com/arangodb/foxx-cli/issues/50)) 13 | 14 | ## [2.1.0] - 2022-10-25 15 | 16 | ## Changed 17 | 18 | - Upgraded to arangojs v8 19 | 20 | ## [2.0.1] - 2022-01-27 21 | 22 | ### Fixed 23 | 24 | - Fixed `Cannot read property 'isDirectory' of null` ([#34](https://github.com/arangodb/foxx-cli/issues/34)) 25 | 26 | ## [2.0.0] - 2020-07-02 27 | 28 | ### Removed 29 | 30 | - Dropped support for Node.js versions older than 12 (LTS) 31 | 32 | ### Changed 33 | 34 | - Changed source directory from `dist` to `lib` 35 | 36 | This should not affect you unless you're using `foxx-cli` internals directly 37 | in your own code. 38 | 39 | ### Added 40 | 41 | - Added support for `--force` to `upgrade` and `replace` 42 | 43 | Using the `--force` flag allows falling back to `install` if the service 44 | does not currently exist. 45 | 46 | ### Fixed 47 | 48 | - Options `--legacy` and `--development` are now passed on correctly 49 | 50 | Previously these options had no effect when using `install`, `replace` 51 | or `upgrade`. 52 | 53 | ## [1.3.0] - 2018-11-07 54 | 55 | ### Changed 56 | 57 | - Server endpoint URLs are no longer pre-processed 58 | 59 | URLs are now handed over to arangojs unaltered to rely on arangojs' URL handling 60 | logic. When using URLs that include credentials or use the `ssl` and `tcp` 61 | alias protocols you may notice that the `server set` command no longer modifies 62 | these and the `.foxxrc` file contains the raw URLs. 63 | 64 | Note that when using URLs that include credentials the credentials will therefore 65 | be printed in plain text when the URL is displayed by a Foxx CLI command (e.g. 66 | `server show` and `server list`). 67 | 68 | ### Added 69 | 70 | - Added support for unix socket URLs ([#32](https://github.com/arangodb/foxx-cli/issues/32)) 71 | 72 | Unix socket URLs are now supported in the following formats: 73 | 74 | - `unix:///socket/path` 75 | - `http+unix:///socket/path` or `https+unix:///socket/path` 76 | - `http://unix:/socket/path` or `https://unix:/socket/path` 77 | - `tcp://unix:/socket/path` or `ssl://unix:/socket/path` 78 | 79 | Note that unix socket URLs can not include credentials. 80 | 81 | ### Fixed 82 | 83 | - Authorization errors now show a prettier error message 84 | 85 | Previously authorization errors were not handled directly and would indicate 86 | a "Code: 11" ArangoError. Now these errors result in a more readable error 87 | message with suggestions for solving the problem. 88 | 89 | ## [1.2.0] - 2018-06-26 90 | 91 | ### Added 92 | 93 | - Option `--password-file` (alias `-p`) reads the password from a file 94 | 95 | This is an alternative to `--password` which is interactive for security reasons. 96 | 97 | ## [1.1.3] - 2018-04-18 98 | 99 | ### Fixed 100 | 101 | - Fixed `foxx init`: generateCrudRoutes is not defined ([#27](https://github.com/arangodb/foxx-cli/issues/27)) 102 | 103 | ## [1.1.2] - 2018-04-11 104 | 105 | ### Fixed 106 | 107 | - `foxx bundle` warning when outfile already exists now shows correct path 108 | 109 | - `foxx init -i` only adds routers to `index.js` when generating CRUD routers 110 | 111 | This fixes a bug where defining collections without also generating CRUD routers 112 | would still result in the routers being referenced in `index.js` leading to a 113 | broken service. 114 | 115 | - Foxx CLI now follows symlinks when generating the service bundle 116 | 117 | ## [1.1.1] - 2018-04-10 118 | 119 | ### Fixed 120 | 121 | - Re-released on Linux to fix bad linebreaks in `foxx` CLI command 122 | 123 | ## [1.1.0] - 2018-04-10 124 | 125 | ### Added 126 | 127 | - `foxx init` makes it easy to create boilerplate for a Foxx service. 128 | 129 | - `foxx add` allows generating various JavaScript files: 130 | 131 | - `foxx add script` generates a script and adds it to the manifest 132 | 133 | - `foxx add test` generates a test suite 134 | 135 | - `foxx add router` generates a router and registers it in the main file 136 | 137 | - `foxx add crud` generates a CRUD router for a collection 138 | 139 | ## [1.0.1] - 2018-03-22 140 | 141 | ### Fixed 142 | 143 | - HTTPS URLs are now resolved correctly 144 | 145 | Foxx CLI now uses the `request` module to download service sources locally. 146 | 147 | ## [1.0.0] - 2018-03-21 148 | 149 | - Initial public release 150 | 151 | [2.1.1]: https://github.com/arangodb/foxx-cli/compare/v2.1.0...v2.1.1 152 | [2.1.0]: https://github.com/arangodb/foxx-cli/compare/v2.0.1...v2.1.0 153 | [2.0.1]: https://github.com/arangodb/foxx-cli/compare/v2.0.0...v2.0.1 154 | [2.0.0]: https://github.com/arangodb/foxx-cli/compare/v1.3.0...v2.0.0 155 | [1.3.0]: https://github.com/arangodb/foxx-cli/compare/v1.2.0...v1.3.0 156 | [1.2.0]: https://github.com/arangodb/foxx-cli/compare/v1.1.3...v1.2.0 157 | [1.1.3]: https://github.com/arangodb/foxx-cli/compare/v1.1.2...v1.1.3 158 | [1.1.2]: https://github.com/arangodb/foxx-cli/compare/v1.1.1...v1.1.2 159 | [1.1.1]: https://github.com/arangodb/foxx-cli/compare/v1.1.0...v1.1.1 160 | [1.1.0]: https://github.com/arangodb/foxx-cli/compare/v1.0.1...v1.1.0 161 | [1.0.1]: https://github.com/arangodb/foxx-cli/compare/v1.0.0...v1.0.1 162 | [1.0.0]: https://github.com/arangodb/foxx-cli/compare/v0.3.1...v1.0.0 163 | -------------------------------------------------------------------------------- /bin/foxx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require("../lib/main"); 3 | -------------------------------------------------------------------------------- /fixtures/echo-script.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arangodb/foxx-cli/84b98de9bad0411579a40cda18d3bbe353e4abe0/fixtures/echo-script.zip -------------------------------------------------------------------------------- /fixtures/minimal-working-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const router = require('@arangodb/foxx/router')(); 3 | module.context.use(router); 4 | router.get((req, res) => { 5 | res.send({hello: 'world'}); 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /fixtures/minimal-working-service.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arangodb/foxx-cli/84b98de9bad0411579a40cda18d3bbe353e4abe0/fixtures/minimal-working-service.zip -------------------------------------------------------------------------------- /fixtures/minimal-working-service/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const router = require('@arangodb/foxx/router')(); 3 | module.context.use(router); 4 | router.get((req, res) => { 5 | res.send({hello: 'world'}); 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /fixtures/minimal-working-service/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimal-working-manifest", 3 | "version": "0.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /fixtures/minimal-working-setup-teardown.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arangodb/foxx-cli/84b98de9bad0411579a40cda18d3bbe353e4abe0/fixtures/minimal-working-setup-teardown.zip -------------------------------------------------------------------------------- /fixtures/passwordFile: -------------------------------------------------------------------------------- 1 | 1234 -------------------------------------------------------------------------------- /fixtures/service-service-service.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arangodb/foxx-cli/84b98de9bad0411579a40cda18d3bbe353e4abe0/fixtures/service-service-service.zip -------------------------------------------------------------------------------- /fixtures/sloppy-service/no-manifest.json: -------------------------------------------------------------------------------- 1 | throw banana; -------------------------------------------------------------------------------- /fixtures/symlink-service/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symlinked-folder-service", 3 | "version": "0.0.0", 4 | "main": "symlink-folder/index.js" 5 | } 6 | -------------------------------------------------------------------------------- /fixtures/symlink-service/recursion: -------------------------------------------------------------------------------- 1 | ../symlink-service -------------------------------------------------------------------------------- /fixtures/symlink-service/symlink-folder: -------------------------------------------------------------------------------- 1 | ../minimal-working-service -------------------------------------------------------------------------------- /fixtures/with-configuration.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arangodb/foxx-cli/84b98de9bad0411579a40cda18d3bbe353e4abe0/fixtures/with-configuration.zip -------------------------------------------------------------------------------- /fixtures/with-dependencies.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arangodb/foxx-cli/84b98de9bad0411579a40cda18d3bbe353e4abe0/fixtures/with-dependencies.zip -------------------------------------------------------------------------------- /fixtures/with-readme.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arangodb/foxx-cli/84b98de9bad0411579a40cda18d3bbe353e4abe0/fixtures/with-readme.zip -------------------------------------------------------------------------------- /fixtures/with-tests.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arangodb/foxx-cli/84b98de9bad0411579a40cda18d3bbe353e4abe0/fixtures/with-tests.zip -------------------------------------------------------------------------------- /lib/bundle.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { load } = require("./ignore"); 3 | const { resolve } = require("path"); 4 | const { walk } = require("./util/fs"); 5 | const { zip } = require("./util/zip"); 6 | 7 | exports.gatherFiles = async function gatherFiles(path) { 8 | const foxxignore = resolve(path, ".foxxignore"); 9 | const shouldIgnore = await load(foxxignore); 10 | return await walk(path, shouldIgnore); 11 | }; 12 | 13 | exports.createBundle = async function createBundle(path, dest) { 14 | const files = await exports.gatherFiles(path); 15 | return await zip(path, files, dest); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/cmds/add.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { common } = require("../util/cli"); 3 | 4 | const command = (exports.command = "add "); 5 | exports.description = "Generate additional service files"; 6 | 7 | const describe = 8 | "Generates additional files for the local service and adds them, depending on the file, to the manifest.json and/or main JavaScript file."; 9 | 10 | exports.builder = (yargs) => 11 | common(yargs, { command, describe }) 12 | .command(require("./add/script")) 13 | .command(require("./add/router")) 14 | .command(require("./add/crud")) 15 | .command(require("./add/test")); 16 | -------------------------------------------------------------------------------- /lib/cmds/add/crud.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { white } = require("chalk"); 3 | const { common } = require("../../util/cli"); 4 | const { fatal } = require("../../util/log"); 5 | const generator = require("../../generator"); 6 | const fs = require("../../util/fs"); 7 | const path = require("path"); 8 | 9 | const command = (exports.command = "crud "); 10 | exports.description = "Add a CRUD router"; 11 | const describe = 12 | 'Creates a router file with CRUD operations for the given collection under "api/.js" and adds it to the main JavaScript file of the service.'; 13 | 14 | const args = [ 15 | ["collection", "Name of the collection for the CRUD operations to be added."], 16 | ]; 17 | 18 | exports.builder = (yargs) => 19 | common(yargs, { command, describe, args }) 20 | .options({ 21 | edge: { 22 | describe: 23 | "Create CRUD operations for an edge collection (different schema validation)", 24 | alias: "e", 25 | type: "boolean", 26 | default: false, 27 | }, 28 | unprefixed: { 29 | describe: "Create CRUD operations for an unprefixed collection", 30 | alias: "u", 31 | type: "boolean", 32 | default: false, 33 | }, 34 | }) 35 | .example( 36 | "$0 add crud kittens", 37 | 'Adds a CRUD router for the collection "kittens" to the local service' 38 | ) 39 | .example( 40 | "$0 add crud kittens -e", 41 | 'Adds a CRUD router for the edge collection "kittens"' 42 | ) 43 | .example( 44 | "$0 add crud kittens -u", 45 | 'Adds a CRUD router for the unprefixed collection "kittens"' 46 | ); 47 | 48 | exports.handler = async function handler(argv) { 49 | const manifestPath = path.resolve(process.cwd(), "manifest.json"); 50 | if (!(await fs.exists(manifestPath))) { 51 | fatal("Current directory does not contain a manifest file."); 52 | } 53 | const cruds = path.resolve(process.cwd(), "api"); 54 | const crud = path.resolve(cruds, `${argv.collection}.js`); 55 | if (await fs.exists(crud)) { 56 | fatal(`Router "${white(crud)}" already exists.`); 57 | } 58 | const manifest = JSON.parse(await fs.readFile(manifestPath)); 59 | const mainPath = path.resolve(process.cwd(), manifest.main || "index.js"); 60 | if (!(await fs.exists(mainPath))) { 61 | await fs.writeFile(mainPath, await generator.generateIndex()); 62 | } 63 | if (!(await fs.exists(cruds))) { 64 | await fs.mkdir(cruds); 65 | } 66 | await fs.writeFile( 67 | crud, 68 | await generator.generateCrud(argv.collection, argv.edge, !argv.unprefixed) 69 | ); 70 | const main = await fs.readFile(mainPath, "utf-8"); 71 | const newMain = `${main.replace(/\n$/, "")}\nmodule.context.use('/${ 72 | argv.collection 73 | }', require('./api/${argv.collection}'), '${argv.collection}');\n`; 74 | await fs.writeFile(mainPath, newMain); 75 | }; 76 | -------------------------------------------------------------------------------- /lib/cmds/add/router.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { white } = require("chalk"); 3 | const { common } = require("../../util/cli"); 4 | const { fatal } = require("../../util/log"); 5 | const generator = require("../../generator"); 6 | const fs = require("../../util/fs"); 7 | const path = require("path"); 8 | 9 | const command = (exports.command = "router "); 10 | exports.description = "Add a router"; 11 | const describe = 12 | 'Creates a router file under "api/.js" and adds it to the main JavaScript file of the service.'; 13 | 14 | const args = [["name", "Name of the router to add."]]; 15 | 16 | exports.builder = (yargs) => 17 | common(yargs, { command, describe, args }).example( 18 | "$0 add router kittens", 19 | 'Adds a router "kittens" to the local service' 20 | ); 21 | 22 | exports.handler = async function handler(argv) { 23 | const manifestPath = path.resolve(process.cwd(), "manifest.json"); 24 | if (!(await fs.exists(manifestPath))) { 25 | fatal("Current directory does not contain a manifest file."); 26 | } 27 | const routers = path.resolve(process.cwd(), "api"); 28 | const router = path.resolve(routers, `${argv.name}.js`); 29 | if (await fs.exists(router)) { 30 | fatal(`Router "${white(router)}" already exists.`); 31 | } 32 | const manifest = JSON.parse(await fs.readFile(manifestPath)); 33 | const mainPath = path.resolve(process.cwd(), manifest.main || "index.js"); 34 | if (!(await fs.exists(mainPath))) { 35 | await fs.writeFile(mainPath, await generator.generateIndex()); 36 | } 37 | if (!(await fs.exists(routers))) { 38 | await fs.mkdir(routers); 39 | } 40 | await fs.writeFile(router, await generator.generateRouter()); 41 | const main = await fs.readFile(mainPath, "utf-8"); 42 | const newMain = `${main.replace(/\n$/, "")}\nmodule.context.use('/${ 43 | argv.name 44 | }', require('./api/${argv.name}'), '${argv.name}');\n`; 45 | await fs.writeFile(mainPath, newMain); 46 | }; 47 | -------------------------------------------------------------------------------- /lib/cmds/add/script.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { white } = require("chalk"); 3 | const { common } = require("../../util/cli"); 4 | const { fatal } = require("../../util/log"); 5 | const { generateScript } = require("../../generator"); 6 | const fs = require("../../util/fs"); 7 | const path = require("path"); 8 | 9 | const command = (exports.command = "script "); 10 | exports.description = "Add a script"; 11 | const describe = 12 | 'Creates a script file under "scripts/.js" and adds it to the manifest.json.'; 13 | 14 | const args = [["name", "Name of the script to add."]]; 15 | 16 | exports.builder = (yargs) => 17 | common(yargs, { command, describe, args }).example( 18 | "$0 add script send-email", 19 | 'Adds a script "send-email" to the local service' 20 | ); 21 | 22 | exports.handler = async function handler(argv) { 23 | const manifestPath = path.resolve(process.cwd(), "manifest.json"); 24 | if (!(await fs.exists(manifestPath))) { 25 | fatal("Current directory does not contain a manifest file."); 26 | } 27 | const scripts = path.resolve(process.cwd(), "scripts"); 28 | if (!(await fs.exists(scripts))) { 29 | await fs.mkdir(scripts); 30 | } 31 | const script = path.resolve(scripts, `${argv.name}.js`); 32 | if (await fs.exists(script)) { 33 | fatal(`Script "${white(script)}" already exists.`); 34 | } 35 | await fs.writeFile(script, await generateScript()); 36 | const manifest = JSON.parse(await fs.readFile(manifestPath)); 37 | if (!manifest.scripts) manifest.scripts = {}; 38 | manifest.scripts[argv.name] = `scripts/${argv.name}.js`; 39 | await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2)); 40 | }; 41 | -------------------------------------------------------------------------------- /lib/cmds/add/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { white } = require("chalk"); 3 | const { common } = require("../../util/cli"); 4 | const { fatal } = require("../../util/log"); 5 | const { generateTest } = require("../../generator"); 6 | const fs = require("../../util/fs"); 7 | const path = require("path"); 8 | 9 | const command = (exports.command = "test "); 10 | exports.detestion = "Add a test"; 11 | const describe = 12 | 'Creates a test file under "tests/.js" and adds the pattern "test/**/*.js" to the manifest.json if its property "test" is undefined.'; 13 | 14 | const args = [["name", "Name of the test to add."]]; 15 | 16 | exports.builder = (yargs) => 17 | common(yargs, { command, describe, args }).example( 18 | "$0 add test example", 19 | 'Adds a test "example" to the local service' 20 | ); 21 | 22 | exports.handler = async function handler(argv) { 23 | const manifestPath = path.resolve(process.cwd(), "manifest.json"); 24 | if (!(await fs.exists(manifestPath))) { 25 | fatal("Current directory does not contain a manifest file."); 26 | } 27 | const tests = path.resolve(process.cwd(), "test"); 28 | if (!(await fs.exists(tests))) { 29 | await fs.mkdir(tests); 30 | } 31 | const test = path.resolve(tests, `${argv.name}.js`); 32 | if (await fs.exists(test)) { 33 | fatal(`Test "${white(test)}" already exists.`); 34 | } 35 | await fs.writeFile(test, await generateTest()); 36 | const manifest = JSON.parse(await fs.readFile(manifestPath)); 37 | if (!manifest.tests) { 38 | manifest.tests = "test/**/*.js"; 39 | await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2)); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /lib/cmds/bundle.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { bold, white } = require("chalk"); 3 | const { resolve } = require("path"); 4 | const { unsplat } = require("../util/array"); 5 | const { common } = require("../util/cli"); 6 | const { exists, safeStat } = require("../util/fs"); 7 | const { fatal } = require("../util/log"); 8 | const { inline: il } = require("../util/text"); 9 | const bundle = require("../bundle").createBundle; 10 | 11 | const command = (exports.command = "bundle [source]"); 12 | exports.description = "Create a service bundle for a service"; 13 | const aliases = (exports.aliases = ["zip"]); 14 | 15 | const describe = 16 | "Creates a zip bundle of a service located on the local filesystem."; 17 | 18 | const args = [ 19 | [ 20 | "source", 21 | "File system path of the service directory to bundle", 22 | '[default: "."]', 23 | ], 24 | ]; 25 | 26 | exports.builder = (yargs) => 27 | common(yargs, { command, aliases, describe, args }) 28 | .options({ 29 | stdout: { 30 | describe: `Write to stdout no matter what stdout is`, 31 | alias: "O", 32 | type: "boolean", 33 | default: false, 34 | }, 35 | outfile: { 36 | describe: 37 | "Write the zip bundle to this file. If omitted, bundle is written to stdout", 38 | alias: "o", 39 | type: "string", 40 | }, 41 | force: { 42 | describe: `If ${bold( 43 | "--outfile" 44 | )} was specified, any existing file will be overwritten.`, 45 | alias: "f", 46 | type: "boolean", 47 | default: false, 48 | }, 49 | sloppy: { 50 | describe: 51 | "Continue even if no manifest file is present in the source directory", 52 | type: "boolean", 53 | default: false, 54 | }, 55 | }) 56 | .example( 57 | "$0 bundle", 58 | "Creates a bundle of the current directory and writes it to stdout" 59 | ) 60 | .example( 61 | "$0 bundle /tmp/service", 62 | "Creates a bundle of the given service directory" 63 | ) 64 | .example("$0 bundle -O", "Writes to stdout even if stdout is a TTY") 65 | .example( 66 | "$0 bundle -o /tmp/bundle.zip", 67 | "Writes the bundle to the given file path" 68 | ) 69 | .example( 70 | "$0 bundle -f -o /tmp/bundle.zip", 71 | "Overwrites the bundle if it already exists" 72 | ) 73 | .example( 74 | "$0 bundle --sloppy", 75 | "Creates the bundle even if service manifest is missing" 76 | ); 77 | 78 | exports.handler = async function handler(argv) { 79 | try { 80 | const source = unsplat(argv.source) || process.cwd(); 81 | let out = unsplat(argv.outfile); 82 | if (!out) { 83 | if (!argv.stdout && process.stdout.isTTY) { 84 | fatal(il` 85 | Refusing to write binary data to stdout. 86 | Use ${bold("--stdout")} if you really want to do this. 87 | `); 88 | } 89 | out = process.stdout; 90 | } else if (argv.stdout) { 91 | fatal(il` 92 | Can't use both ${bold("--outfile")} 93 | and ${bold("--stdout")} at the same time. 94 | `); 95 | } else if (!argv.force) { 96 | const stats = await safeStat(out); 97 | if (stats) { 98 | fatal(il` 99 | Outfile "${white(out)}" already exists. 100 | Use ${bold("--force")} to overwrite existing file. 101 | `); 102 | } 103 | } 104 | const stats = await safeStat(source); 105 | if (!stats) { 106 | fatal(`Source directory "${white(source)}" does not exist.`); 107 | } else if (!stats.isDirectory()) { 108 | fatal(`Source directory "${white(source)}" is not a directory.`); 109 | } 110 | if (!argv.sloppy) { 111 | let path = resolve(source, "manifest.json"); 112 | if (!(await exists(path))) { 113 | fatal(il` 114 | Source directory "${white(source)}" does not contain a manifest file. 115 | Use ${bold("--sloppy")} if you want to skip this check. 116 | `); 117 | } 118 | } 119 | await bundle(source, out); 120 | } catch (e) { 121 | fatal(e); 122 | } 123 | }; 124 | -------------------------------------------------------------------------------- /lib/cmds/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { ERROR_SERVICE_NOT_FOUND } = require("../errors"); 3 | const { common, serverArgs } = require("../util/cli"); 4 | const { error, fatal, info, json } = require("../util/log"); 5 | 6 | const { bold, gray, white } = require("chalk"); 7 | const client = require("../util/client"); 8 | const { inline: il } = require("../util/text"); 9 | const parseOptions = require("../util/parseOptions"); 10 | const resolveServer = require("../resolveServer"); 11 | const streamToBuffer = require("../util/streamToBuffer"); 12 | 13 | const command = (exports.command = "config [options..]"); 14 | exports.description = "Manage the configuration of a mounted service"; 15 | const aliases = (exports.aliases = ["configuration", "cfg"]); 16 | 17 | const describe = il`Updates or fetches the current configuration for the service at the given ${bold( 18 | "mount" 19 | )} path. 20 | 21 | Returns an object mapping the configuration option names to their definitions including a human-friendly title and the current value (if any).`; 22 | 23 | const args = [ 24 | ["mount", "Mount path of the service"], 25 | [ 26 | "options", 27 | `Key-value pairs to apply to the configuration. Use ${bold( 28 | "@" 29 | )} to pass a JSON file from stdin`, 30 | ], 31 | ]; 32 | 33 | exports.builder = (yargs) => 34 | common(yargs, { command, aliases, describe, args }) 35 | .options({ 36 | ...serverArgs, 37 | force: { 38 | describe: il` 39 | Clear existing values for any omitted configuration options. 40 | Note that clearing required options with no default value will 41 | result in the service being disabled until new values are provided. 42 | `, 43 | alias: "f", 44 | type: "boolean", 45 | default: false, 46 | }, 47 | raw: { 48 | describe: "Output service configuration as raw JSON", 49 | type: "boolean", 50 | default: false, 51 | }, 52 | minimal: { 53 | describe: "Print minimal output", 54 | type: "boolean", 55 | default: false, 56 | }, 57 | }) 58 | .example( 59 | "$0 config /myfoxx", 60 | 'Shows the configuration for the mounted service at the URL "/myfoxx"' 61 | ) 62 | .example( 63 | "$0 config /myfoxx someNumber=23", 64 | 'Sets the "someNumber" configuration option to the number 23' 65 | ) 66 | .example( 67 | "$0 config /myfoxx -f someNumber=23", 68 | 'Sets the "someNumber" configuration option and clears all other options' 69 | ) 70 | .example( 71 | "echo '{\"someNumber\": 23}' | $0 config /myfoxx @", 72 | "Sets the configuration using JSON data from stdin" 73 | ) 74 | .example("$0 config /myfoxx -f", "Clears the service configuration"); 75 | 76 | exports.handler = async function handler(argv) { 77 | let options = parseOptions(argv.options); 78 | if (!options && argv.force) { 79 | options = {}; 80 | } else if (options === "@") { 81 | const output = await streamToBuffer(process.stdin); 82 | let json; 83 | try { 84 | json = output.toString("utf-8"); 85 | } catch (e) { 86 | fatal("Not a valid JSON string"); 87 | } 88 | try { 89 | options = JSON.parse(json); 90 | } catch (e) { 91 | fatal(e.message); 92 | } 93 | } 94 | try { 95 | const server = await resolveServer(argv); 96 | const db = client(server); 97 | let result; 98 | if (!options) { 99 | result = await db.getServiceConfiguration(argv.mount); 100 | } else if (argv.force) { 101 | result = await db.replaceServiceConfiguration(argv.mount, options); 102 | } else { 103 | result = await db.updateServiceConfiguration(argv.mount, options); 104 | } 105 | if (argv.raw) { 106 | if (argv.minimal) { 107 | result = Object.keys(result).reduce( 108 | (obj, key) => { 109 | obj.values[key] = result[key].current; 110 | if (result[key].warning) { 111 | if (!obj.warnings) obj.warnings = {}; 112 | obj.warnings[key] = result[key].warning; 113 | } 114 | return obj; 115 | }, 116 | { values: {} } 117 | ); 118 | if (!options) json(result.values); 119 | else json(result); 120 | } else json(result); 121 | } else if (argv.minimal) { 122 | for (const key of Object.keys(result)) { 123 | const dfn = result[key]; 124 | if (dfn.warning) error(`${key}: ${dfn.warning}`); 125 | if (dfn.current === undefined) info(`${key}: ${gray("N/A")}`); 126 | else info(`${key}: ${dfn.current}`); 127 | } 128 | } else { 129 | let i = Object.keys(result).length; 130 | for (const key of Object.keys(result)) { 131 | const dfn = result[key]; 132 | info(bold(dfn.title)); 133 | info(`Key: ${key}`); 134 | const parts = [`Type: ${dfn.type}`]; 135 | if (!dfn.required) parts.push(gray("(optional)")); 136 | info(parts.join(" ")); 137 | if (dfn.current === undefined) info(`Value: ${gray("N/A")}`); 138 | else info(`Value: ${dfn.current}`); 139 | info(dfn.description); 140 | if (i-- > 1) info(""); 141 | } 142 | } 143 | } catch (e) { 144 | if (e.isArangoError && e.errorNum === ERROR_SERVICE_NOT_FOUND) { 145 | fatal(`No service found at "${white(argv.mount)}"`); 146 | } 147 | fatal(e); 148 | } 149 | }; 150 | -------------------------------------------------------------------------------- /lib/cmds/deps.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { ERROR_SERVICE_NOT_FOUND } = require("../errors"); 3 | const { error, info, fatal, json } = require("../util/log"); 4 | 5 | const { bold, gray, red, white } = require("chalk"); 6 | const client = require("../util/client"); 7 | const { common, serverArgs } = require("../util/cli"); 8 | const { inline: il } = require("../util/text"); 9 | const parseOptions = require("../util/parseOptions"); 10 | const resolveServer = require("../resolveServer"); 11 | const streamToBuffer = require("../util/streamToBuffer"); 12 | 13 | const command = (exports.command = "deps [options..]"); 14 | exports.description = "Manage the dependencies of a mounted service"; 15 | const aliases = (exports.aliases = ["dependencies", "dep"]); 16 | 17 | const describe = il`Updates or fetches the current dependencies for service at the given ${bold( 18 | "mount" 19 | )} path. 20 | 21 | Returns an object mapping the dependency names to their definitions including a human-friendly title and the current mount path (if any).`; 22 | 23 | const args = [ 24 | ["mount", "Mount path of the service"], 25 | [ 26 | "options", 27 | `Key-value pairs to apply to the dependencies. Use ${bold( 28 | "@" 29 | )} to pass a JSON file from stdin`, 30 | ], 31 | ]; 32 | 33 | exports.builder = (yargs) => 34 | common(yargs, { command, aliases, describe, args }) 35 | .options({ 36 | ...serverArgs, 37 | force: { 38 | describe: il` 39 | Clear existing values for any omitted dependencies. 40 | Note that clearing required dependencies will result in 41 | the service being disabled until new values are provided. 42 | `, 43 | alias: "f", 44 | type: "boolean", 45 | default: false, 46 | }, 47 | raw: { 48 | describe: "Output service dependencies as raw JSON", 49 | type: "boolean", 50 | default: false, 51 | }, 52 | minimal: { 53 | describe: "Print minimal output", 54 | type: "boolean", 55 | default: false, 56 | }, 57 | }) 58 | .example( 59 | "$0 deps /myfoxx", 60 | 'Show the dependencies for the service mounted at "/foxxmail"' 61 | ) 62 | .example( 63 | "$0 deps /myfoxx mailer=/foxxmail", 64 | 'Sets the "mailer" dependency to the service mounted at "/foxxmail"' 65 | ) 66 | .example( 67 | "$0 deps /myfoxx -f mailer=/foxxmail", 68 | 'Sets the "mailer" dependency and clears any other dependencies' 69 | ) 70 | .example( 71 | 'echo \'{"mailer": "/foxxmail"}\' | $0 deps /myfoxx @', 72 | "Sets the dependency using JSON data from stdin" 73 | ) 74 | .example("$0 deps /myfoxx -f", "Clears all configured dependencies"); 75 | 76 | exports.handler = async function handler(argv) { 77 | let options = parseOptions(argv.options); 78 | if (!options && argv.force) { 79 | options = {}; 80 | } else if (options === "@") { 81 | const output = await streamToBuffer(process.stdin); 82 | let json; 83 | try { 84 | json = output.toString("utf-8"); 85 | } catch (e) { 86 | fatal("Not a valid JSON string"); 87 | } 88 | try { 89 | options = JSON.parse(json); 90 | } catch (e) { 91 | fatal(e.message); 92 | } 93 | } 94 | try { 95 | const server = await resolveServer(argv); 96 | const db = client(server); 97 | let result; 98 | if (!options) { 99 | result = await db.getServiceDependencies(argv.mount); 100 | } else if (argv.force) { 101 | result = await db.replaceServiceDependencies(argv.mount, options); 102 | } else { 103 | result = await db.updateServiceDependencies(argv.mount, options); 104 | } 105 | if (argv.raw) { 106 | if (argv.minimal) { 107 | result = Object.keys(result).reduce( 108 | (obj, key) => { 109 | obj.values[key] = result[key].current; 110 | if (result[key].warning) { 111 | if (!obj.warnings) obj.warnings = {}; 112 | obj.warnings[key] = result[key].warning; 113 | } 114 | return obj; 115 | }, 116 | { values: {} } 117 | ); 118 | if (!options) json(result.values); 119 | else json(result); 120 | } else json(result); 121 | } else if (argv.minimal) { 122 | for (const key of Object.keys(result)) { 123 | const dfn = result[key]; 124 | if (dfn.warning) error(`${key}: ${dfn.warning}`); 125 | if (dfn.current === undefined) info(`${key}: ${gray("N/A")}`); 126 | else info(`${key}: ${dfn.current}`); 127 | } 128 | } else { 129 | let i = Object.keys(result).length; 130 | for (const key of Object.keys(result)) { 131 | const dfn = result[key]; 132 | info(bold(dfn.title)); 133 | info(`Key: ${key}`); 134 | const parts = [`Depends: ${dfn.name}@${dfn.version}`]; 135 | if (!dfn.required) parts.push(gray("(optional)")); 136 | if (dfn.multiple) parts.push(red("(multi)")); 137 | info(parts.join(" ")); 138 | if (dfn.current === undefined) info(`Mount: ${gray("N/A")}`); 139 | else info(`Mount: ${dfn.current}`); 140 | info(dfn.description); 141 | if (i-- > 1) info(""); 142 | } 143 | } 144 | } catch (e) { 145 | if (e.isArangoError && e.errorNum === ERROR_SERVICE_NOT_FOUND) { 146 | fatal(`No service found at "${white(argv.mount)}"`); 147 | } 148 | fatal(e); 149 | } 150 | }; 151 | -------------------------------------------------------------------------------- /lib/cmds/download.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { ERROR_SERVICE_NOT_FOUND } = require("../errors"); 3 | const { bold, white } = require("chalk"); 4 | const { common, serverArgs } = require("../util/cli"); 5 | const { createWriteStream, existsSync } = require("fs"); 6 | const { fatal, info } = require("../util/log"); 7 | const { exists, readdir, safeStat } = require("../util/fs"); 8 | 9 | const client = require("../util/client"); 10 | const { extractBuffer } = require("../util/zip"); 11 | const { inline: il } = require("../util/text"); 12 | const { resolve } = require("path"); 13 | const resolveServer = require("../resolveServer"); 14 | const { unsplat } = require("../util/array"); 15 | 16 | const command = (exports.command = "download "); 17 | exports.description = "Download a mounted service"; 18 | const aliases = (exports.aliases = ["dl"]); 19 | 20 | const describe = il`Downloads a zip bundle of the service directory. 21 | 22 | When development mode is enabled, this always creates a new bundle. Otherwise the bundle will represent the version of a service that is installed on that ArangoDB instance.`; 23 | 24 | const args = [["mount", "Mount path of the service"]]; 25 | 26 | exports.builder = (yargs) => 27 | common(yargs, { command, aliases, describe, args }) 28 | .options({ 29 | ...serverArgs, 30 | stdout: { 31 | describe: `Write to stdout no matter what stdout is`, 32 | alias: "O", 33 | type: "boolean", 34 | default: false, 35 | }, 36 | outfile: { 37 | describe: 38 | "Write or extract the bundle to this path. If omitted, bundle will be written to stdout or extracted to the current working directory", 39 | alias: "o", 40 | type: "string", 41 | }, 42 | extract: { 43 | describe: "Extract zip bundle instead of just downloading it", 44 | alias: "x", 45 | type: "boolean", 46 | default: false, 47 | }, 48 | force: { 49 | describe: `If ${bold("--outfile")} and/or ${bold( 50 | "--extract" 51 | )} were specified, any existing files will be overwritten.`, 52 | alias: "f", 53 | type: "boolean", 54 | default: false, 55 | }, 56 | }) 57 | .example( 58 | "$0 download /hello", 59 | 'Downloads the Foxx service mounted at the URL "/hello" and writes the bundle to stdout' 60 | ) 61 | .example( 62 | "$0 download -x /hello", 63 | "Extracts the bundle to the current directory" 64 | ) 65 | .example( 66 | "$0 download /hello -o hello.zip ", 67 | 'Writes the bundle to the file "hello.zip"' 68 | ) 69 | .example( 70 | "$0 download -f /hello -o hello.zip ", 71 | 'Writes the bundle to "hello.zip" even if that file already exists' 72 | ) 73 | .example( 74 | "$0 download -x /hello -o /tmp/hello", 75 | 'Extracts the bundle to the directory "/tmp/hello"' 76 | ) 77 | .example( 78 | "$0 download -xf /hello -o /tmp/hello", 79 | "Extracts the bundle and overwrites any existing files" 80 | ); 81 | 82 | exports.handler = async function handler(argv) { 83 | argv.outfile = unsplat(argv.outfile); 84 | let out, outdir; 85 | if (!argv.outfile) { 86 | if (!argv.extract) { 87 | if (!argv.stdout && process.stdout.isTTY) { 88 | fatal(il` 89 | Refusing to write binary data to stdout. 90 | Use ${bold("--stdout")} if you really want to do this. 91 | `); 92 | } 93 | out = process.stdout; 94 | } else if (argv.stdout) { 95 | fatal(il` 96 | Can't use both ${bold("--extract")} 97 | and ${bold("--stdout")} at the same time. 98 | `); 99 | } else { 100 | outdir = process.cwd(); 101 | } 102 | } else if (argv.stdout) { 103 | fatal(il` 104 | Can't use both ${bold("--outfile")} 105 | and ${bold("--stdout")} at the same time. 106 | `); 107 | } else if (argv.extract) { 108 | outdir = resolve(argv.outfile); 109 | const stats = await safeStat(outdir); 110 | if (stats) { 111 | if (!stats.isDirectory()) { 112 | fatal(`Can't extract to "${white(argv.outfile)}": not a directory.`); 113 | } 114 | if (!argv.force && (await readdir(outdir)).length) { 115 | fatal(il` 116 | Refusing to extract to non-empty directory "${white(argv.outfile)}". 117 | Use ${bold("--force")} to overwrite existing files. 118 | `); 119 | } 120 | } 121 | } else { 122 | if (!argv.force && (await exists(argv.outfile))) { 123 | fatal(il` 124 | Refusing to overwrite existing file "${white(argv.outfile)}". 125 | Use ${bold("--force")} to overwrite existing file. 126 | `); 127 | } 128 | out = createWriteStream(argv.outfile); 129 | } 130 | try { 131 | const server = await resolveServer(argv); 132 | const db = client(server); 133 | const bundle = await db.downloadService(argv.mount); 134 | if (!argv.extract) { 135 | out.write(bundle); 136 | if (out !== process.stdout) { 137 | out.end(); 138 | if (argv.verbose) { 139 | info(`Created "${argv.outfile}".`); 140 | } 141 | } 142 | } else { 143 | await extractBuffer(bundle, { 144 | dir: outdir, 145 | onEntry(entry) { 146 | if (existsSync(resolve(outdir, entry.fileName))) { 147 | info(`Overwriting "${entry.fileName}" …`); 148 | } else if (argv.verbose) { 149 | info(`Creating "${entry.fileName}" …`); 150 | } 151 | }, 152 | }); 153 | if (argv.verbose) { 154 | info("Done."); 155 | } 156 | } 157 | } catch (e) { 158 | if (e.isArangoError && e.errorNum === ERROR_SERVICE_NOT_FOUND) { 159 | fatal(`No service found at "${white(argv.mount)}".`); 160 | } 161 | fatal(e); 162 | } 163 | }; 164 | -------------------------------------------------------------------------------- /lib/cmds/ignore.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { resolve } = require("path"); 3 | const { common } = require("../util/cli"); 4 | const { fatal } = require("../util/text"); 5 | const { save: saveIgnore } = require("../ignore"); 6 | 7 | const command = (exports.command = "ignore [patterns..]"); 8 | exports.description = "Add one or more patterns to the .foxxignore file"; 9 | const aliases = (exports.aliases = ["exclude"]); 10 | 11 | const describe = 12 | "Add one or more patterns to the .foxxingore file which is used to exclude files from being archived with the command bundle."; 13 | 14 | const args = [["patterns", "Patterns to add to the .foxxignore file"]]; 15 | 16 | exports.builder = (yargs) => 17 | common(yargs, { command, aliases, describe, args }) 18 | .options({ 19 | force: { 20 | describe: "Overwrite existing patterns (including defaults)", 21 | alias: "f", 22 | type: "boolean", 23 | default: false, 24 | }, 25 | }) 26 | .example( 27 | "$0 ignore", 28 | "Creates a .foxxignore file with defaults in the current directory if it does not already exist" 29 | ) 30 | .example( 31 | "$0 ignore example/", 32 | 'Adds the pattern for a directory "example" to the .foxxignore file' 33 | ) 34 | .example( 35 | "$0 ignore example.md", 36 | 'Adds the pattern for a file "example.md" to the .foxxignore file' 37 | ) 38 | .example("$0 ignore example/ example.md", "Adds multiple patterns") 39 | .example( 40 | "$0 ignore *.md", 41 | 'Adds a pattern to ignore all files ending with ".md"' 42 | ) 43 | .example( 44 | "$0 ignore -f example/", 45 | "Overwrites all patterns with the given one" 46 | ) 47 | .example("$0 ignore -f", "Removes all patterns from .foxxignore"); 48 | 49 | exports.handler = async function handler(argv) { 50 | const foxxignore = resolve(process.cwd(), ".foxxignore"); 51 | try { 52 | await saveIgnore(foxxignore, argv.patterns, argv.force); 53 | } catch (e) { 54 | fatal(e); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /lib/cmds/init.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { white } = require("chalk"); 3 | const { common } = require("../util/cli"); 4 | const { fatal } = require("../util/log"); 5 | const { generateFiles } = require("../generator"); 6 | const wizard = require("../generator/wizard"); 7 | const fs = require("../util/fs"); 8 | const path = require("path"); 9 | 10 | const command = (exports.command = "init [dest]"); 11 | exports.description = "Create a new Foxx service"; 12 | 13 | const describe = "Creates a new Foxx service in the given file system path."; 14 | 15 | const args = [ 16 | ["dest", "File system path of the service to create.", '[default: "."]'], 17 | ]; 18 | 19 | exports.builder = (yargs) => 20 | common(yargs, { command, describe, args }) 21 | .options({ 22 | example: { 23 | describe: "Generate example code", 24 | alias: "e", 25 | type: "boolean", 26 | default: false, 27 | }, 28 | interactive: { 29 | describe: "Prompt for input instead of using default values", 30 | alias: "i", 31 | type: "boolean", 32 | default: false, 33 | }, 34 | }) 35 | .example( 36 | "$0 init", 37 | "Create a new Foxx service in the current directory using default values" 38 | ) 39 | .example( 40 | "$0 init /tmp/my-service", 41 | 'Create a new Foxx service in directory "/tmp/my-service" using default values' 42 | ) 43 | .example( 44 | "$0 init --interactive", 45 | "Create a new Foxx service prompting for input" 46 | ) 47 | .example("$0 init --example", "Create a new example Foxx service"); 48 | 49 | exports.handler = async function handler(argv) { 50 | const dest = argv.dest ? argv.dest : process.cwd(); 51 | const stats = await fs.safeStat(dest); 52 | if (!stats) { 53 | await fs.mkdir(path.resolve(dest)); 54 | } else if (!stats.isDirectory()) { 55 | fatal(`Destination "${white(dest)}" is not a directory.`); 56 | } 57 | if ((await fs.readdir(dest)).length > 0) { 58 | fatal(`Refusing to write to non-empty directory "${white(dest)}".`); 59 | } 60 | let options = { 61 | cwd: dest, 62 | example: argv.example && !argv.interactive, 63 | name: path.basename(dest), 64 | version: "0.0.0", 65 | mainFile: "index.js", 66 | engineVersion: "^3.0.0", 67 | tests: "test/**/*.js", 68 | }; 69 | if (options.example) { 70 | options.name = "hello-world"; 71 | options.authorName = "ArangoDB GmbH"; 72 | options.license = "Apache-2.0"; 73 | options.description = "A simple Hello World Foxx service"; 74 | } 75 | if (argv.interactive) { 76 | options = Object.assign(options, await wizard(options)); 77 | } 78 | try { 79 | const files = await generateFiles(options); 80 | await Promise.all([ 81 | fs.mkdir(path.resolve(dest, "api")), 82 | fs.mkdir(path.resolve(dest, "scripts")), 83 | fs.mkdir(path.resolve(dest, "test")), 84 | ]); 85 | await Promise.all( 86 | files.map((file) => 87 | fs.writeFile(path.resolve(dest, file.name), file.content) 88 | ) 89 | ); 90 | } catch (e) { 91 | fatal(e); 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /lib/cmds/install.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const errors = require("../errors"); 3 | const { common, parseServiceOptions, serverArgs } = require("../util/cli"); 4 | const { fatal, info, json } = require("../util/log"); 5 | 6 | const { bold, white } = require("chalk"); 7 | const client = require("../util/client"); 8 | const resolveServer = require("../resolveServer"); 9 | const resolveToStream = require("../resolveToStream"); 10 | 11 | const command = (exports.command = "install [source]"); 12 | exports.description = "Install a service at a given mount path"; 13 | const aliases = (exports.aliases = ["i"]); 14 | 15 | const describe = `Installs the given new service at the given ${bold( 16 | "mount" 17 | )} path.`; 18 | 19 | const args = [ 20 | ["mount", "Mount path of the service"], 21 | [ 22 | "source", 23 | `URL or file system path of the service to install. Use ${bold( 24 | "@" 25 | )} to pass a zip file from stdin`, 26 | '[default: "."]', 27 | ], 28 | ]; 29 | 30 | exports.builder = (yargs) => 31 | common(yargs, { command, aliases, describe, args }) 32 | .options({ 33 | ...serverArgs, 34 | setup: { 35 | describe: `Run the setup script after installing the service. Use ${bold( 36 | "--no-setup" 37 | )} to disable`, 38 | type: "boolean", 39 | default: true, 40 | }, 41 | development: { 42 | describe: 43 | "Install the service in development mode. You can edit the service's files on the server and changes will be reflected automatically", 44 | alias: "dev", 45 | type: "boolean", 46 | default: false, 47 | }, 48 | legacy: { 49 | describe: 50 | "Install the service in legacy compatibility mode for legacy services written for ArangoDB 2.8 and earlier", 51 | type: "boolean", 52 | default: false, 53 | }, 54 | remote: { 55 | describe: `Let the ArangoDB server resolve ${bold( 56 | "source" 57 | )} instead of resolving it locally`, 58 | alias: "R", 59 | type: "boolean", 60 | default: false, 61 | }, 62 | cfg: { 63 | describe: 64 | "Pass a configuration option as a name=value pair. This option can be specified multiple times", 65 | alias: "c", 66 | type: "string", 67 | }, 68 | dep: { 69 | describe: 70 | "Pass a dependency option as a name=/path pair. This option can be specified multiple times", 71 | alias: "d", 72 | type: "string", 73 | }, 74 | }) 75 | .example( 76 | "$0 install /hello", 77 | 'Install the current working directory as a Foxx service at the URL "/hello"' 78 | ) 79 | .example( 80 | "$0 install --dev /hello", 81 | "Install the service in development mode" 82 | ) 83 | .example( 84 | "$0 install --server http://localhost:8530 /hello", 85 | "Use the server on port 8530 instead of the default" 86 | ) 87 | .example( 88 | "$0 install --database mydb /hello", 89 | 'Use the database "mydb" instead of the default' 90 | ) 91 | .example( 92 | "$0 install --server dev /hello", 93 | 'Use the "dev" server instead of the default. See the "server" command for details' 94 | ) 95 | .example( 96 | "$0 install --no-setup /hello", 97 | "Install the service without running the setup script afterwards" 98 | ) 99 | .example("$0 install /hello demo.zip", 'Install the bundle "demo.zip"') 100 | .example( 101 | "$0 install /hello /tmp/bundle.zip", 102 | 'Install the bundle located at "/tmp/bundle.zip" (on the local machine)' 103 | ) 104 | .example( 105 | "$0 install /hello /tmp/my-service", 106 | 'Bundle and install the directory "/tmp/my-service" (on the local machine)' 107 | ) 108 | .example( 109 | "$0 install /hello -R /tmp/bundle.zip", 110 | 'Install the bundle located at "/tmp/bundle.zip" (on the ArangoDB server)' 111 | ) 112 | .example( 113 | "$0 install /hello http://example.com/foxx.zip", 114 | 'Download the bundle from "http://example.com/foxx.zip" locally and install it' 115 | ) 116 | .example( 117 | "$0 install /hello -R http://example.com/foxx.zip", 118 | 'Instruct the ArangoDB server to download the bundle from "http://example.com/foxx.zip" and install it' 119 | ) 120 | .example( 121 | "$0 install /hello -d mailer=/mymail -d auth=/myauth", 122 | 'Install the service and set its "mailer" and "auth" dependencies' 123 | ) 124 | .example( 125 | "cat foxx.zip | $0 install /hello @", 126 | "Install the bundle read from stdin" 127 | ); 128 | 129 | exports.handler = async function handler(argv) { 130 | try { 131 | const opts = parseServiceOptions(argv); 132 | const server = await resolveServer(argv); 133 | const source = argv.remote 134 | ? argv.source 135 | : await resolveToStream(argv.source); 136 | const db = client(server); 137 | const result = await db.installService(argv.mount, source, { 138 | ...opts, 139 | development: argv.development, 140 | legacy: argv.legacy, 141 | setup: argv.setup, 142 | }); 143 | if (argv.raw) { 144 | json(result); 145 | } else { 146 | info(`Installed service at "${white(argv.mount)}".`); 147 | } 148 | } catch (e) { 149 | if (e.isArangoError) { 150 | switch (e.errorNum) { 151 | case errors.ERROR_INVALID_MOUNTPOINT: 152 | fatal(`Not a valid mount path: "${white(argv.mount)}".`); 153 | break; 154 | case errors.ERROR_SERVICE_MOUNTPOINT_CONFLICT: 155 | fatal(`Mount path already in use: "${white(argv.mount)}".`); 156 | break; 157 | case errors.ERROR_SERVICE_SOURCE_NOT_FOUND: 158 | fatal(`Server failed to resolve source "${white(argv.source)}".`); 159 | break; 160 | case errors.ERROR_SERVICE_SOURCE_ERROR: 161 | fatal(`Server failed to download source "${white(argv.source)}".`); 162 | break; 163 | case errors.ERROR_SERVICE_MANIFEST_NOT_FOUND: 164 | fatal("Service bundle does not contain a manifest."); 165 | break; 166 | case errors.ERROR_MALFORMED_MANIFEST_FILE: 167 | fatal("Service manifest is not a well-formed JSON file."); 168 | break; 169 | case errors.ERROR_INVALID_SERVICE_MANIFEST: 170 | fatal(`Service manifest rejected due to errors:\n\n${e.message}`); 171 | break; 172 | case errors.ERROR_MODULE_NOT_FOUND: 173 | fatal( 174 | `Server encountered errors trying to locate a JavaScript file:\n\n${e.message}\n\nMake sure the service bundle includes all files referenced in the manifest.` 175 | ); 176 | break; 177 | case errors.ERROR_MODULE_FAILURE: 178 | fatal( 179 | `Server encountered errors executing a JavaScript file:\n\n${e.message}\n\nFor details check the arangod server logs.` 180 | ); 181 | break; 182 | case errors.ERROR_MODULE_SYNTAX_ERROR: 183 | fatal( 184 | `Server encountered errors trying to parse a JavaScript file:\n\n${e.message}` 185 | ); 186 | break; 187 | } 188 | } 189 | fatal(e); 190 | } 191 | }; 192 | -------------------------------------------------------------------------------- /lib/cmds/list.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { bold, gray } = require("chalk"); 3 | const { common, serverArgs } = require("../util/cli"); 4 | const { detail, fatal, info, json } = require("../util/log"); 5 | 6 | const client = require("../util/client"); 7 | const { group } = require("../util/text"); 8 | const resolveServer = require("../resolveServer"); 9 | 10 | const command = (exports.command = "list"); 11 | exports.description = "List mounted services"; 12 | const aliases = (exports.aliases = ["ls"]); 13 | 14 | const describe = "Shows an overview of all installed services."; 15 | 16 | exports.builder = (yargs) => 17 | common(yargs, { command, aliases, describe }) 18 | .options({ 19 | ...serverArgs, 20 | all: { 21 | describe: "Include system services", 22 | alias: "a", 23 | type: "boolean", 24 | default: false, 25 | }, 26 | raw: { 27 | describe: "Output raw JSON responses", 28 | type: "boolean", 29 | default: false, 30 | }, 31 | }) 32 | .example( 33 | "$0 list", 34 | "Shows all installed services not including system services" 35 | ) 36 | .example( 37 | "$0 list -a", 38 | "Shows all installed services including system services" 39 | ) 40 | .example( 41 | "$0 list --server http://localhost:8530", 42 | "Use the server on port 8530 instead of the default" 43 | ) 44 | .example( 45 | "$0 list --database mydb", 46 | 'Use the database "mydb" instead of the default' 47 | ); 48 | 49 | exports.handler = async function handler(argv) { 50 | try { 51 | const server = await resolveServer(argv); 52 | const db = client(server); 53 | let services = await db.listServices(); 54 | if (!argv.all) { 55 | services = services.filter((service) => !service.mount.startsWith("/_")); 56 | } 57 | if (argv.raw) { 58 | json(services); 59 | } else if (services.length) { 60 | info( 61 | group( 62 | ...services.map((service) => [ 63 | service.development ? bold(service.mount) : service.mount, 64 | prettyVersion(service), 65 | ]) 66 | ) 67 | ); 68 | } else if (argv.verbose) { 69 | detail("No services available."); 70 | } 71 | } catch (e) { 72 | fatal(e); 73 | } 74 | }; 75 | 76 | function prettyVersion(service) { 77 | let parts = []; 78 | if (service.name && service.version) { 79 | parts.push(`${service.name}@${service.version}`); 80 | } else { 81 | if (service.name) parts.push(service.name); 82 | if (service.version) parts.push(service.version); 83 | } 84 | if (service.legacy) parts.push(gray("(legacy)")); 85 | if (service.development) parts.push(bold("[DEV]")); 86 | return parts.join(" "); 87 | } 88 | -------------------------------------------------------------------------------- /lib/cmds/run.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const errors = require("../errors"); 3 | const { json, fatal } = require("../util/log"); 4 | 5 | const { bold, white } = require("chalk"); 6 | const client = require("../util/client"); 7 | const { common, serverArgs } = require("../util/cli"); 8 | const resolveServer = require("../resolveServer"); 9 | const streamToBuffer = require("../util/streamToBuffer"); 10 | 11 | const command = (exports.command = "run [options]"); 12 | exports.description = "Run a script for a mounted service"; 13 | const aliases = (exports.aliases = ["script"]); 14 | 15 | const describe = `Runs the given script for the service at the given ${bold( 16 | "mount" 17 | )} path. Returns the exports of the script, if any.`; 18 | 19 | const args = [ 20 | ["mount", "Mount path of the service"], 21 | ["name", "Name of the script to execute"], 22 | ["options", "Arguments that will be passed to the script"], 23 | ]; 24 | 25 | exports.builder = (yargs) => 26 | common(yargs, { command, aliases, describe, args }) 27 | .options({ ...serverArgs }) 28 | .example( 29 | "$0 run /hello send-email", 30 | 'Runs a script "send-email" of the service at the URL "/hello"' 31 | ) 32 | .example( 33 | `$0 run /hello send-email '{"hello": "world"}'`, 34 | "Pass an argument to the script" 35 | ) 36 | .example( 37 | "$0 run /hello send-email --server http://localhost:8530", 38 | "Use the server on port 8530 instead of the default" 39 | ) 40 | .example( 41 | "$0 run /hello send-email --database mydb", 42 | 'Use the database "mydb" instead of the default' 43 | ); 44 | 45 | exports.handler = async function handler(argv) { 46 | let options = argv.options; 47 | if (!options) { 48 | options = undefined; 49 | } else if (options === "@") { 50 | const output = await streamToBuffer(process.stdin); 51 | let json; 52 | try { 53 | json = output.toString("utf-8"); 54 | } catch (e) { 55 | fatal("Not a valid JSON string"); 56 | } 57 | try { 58 | options = JSON.parse(json); 59 | } catch (e) { 60 | fatal(e.message); 61 | } 62 | } else { 63 | try { 64 | options = JSON.parse(options); 65 | } catch (e) { 66 | fatal(e.message); 67 | } 68 | } 69 | try { 70 | const server = await resolveServer(argv); 71 | const db = client(server); 72 | const result = await db.runServiceScript(argv.mount, argv.name, options); 73 | json(result); 74 | } catch (e) { 75 | if (e.isArangoError) { 76 | switch (e.errorNum) { 77 | case errors.ERROR_SERVICE_NOT_FOUND: 78 | fatal(`No service found at "${white(argv.mount)}".`); 79 | break; 80 | case errors.ERROR_SERVICE_NEEDS_CONFIGURATION: 81 | fatal( 82 | `Service at "${white( 83 | argv.mount 84 | )}" is missing configuration or dependencies.` 85 | ); 86 | break; 87 | case errors.ERROR_SERVICE_UNKNOWN_SCRIPT: 88 | fatal(`Service does not have a script called "${white(argv.name)}".`); 89 | break; 90 | case errors.ERROR_MODULE_NOT_FOUND: 91 | fatal( 92 | `Server encountered errors trying to locate a JavaScript file:\n\n${e.message}\n\nMake sure the service bundle includes all files referenced in the manifest.` 93 | ); 94 | break; 95 | case errors.ERROR_MODULE_FAILURE: 96 | fatal( 97 | `Server encountered errors executing a JavaScript file:\n\n${e.message}\n\nFor details check the arangod server logs.` 98 | ); 99 | break; 100 | case errors.ERROR_MODULE_SYNTAX_ERROR: 101 | fatal( 102 | `Server encountered errors trying to parse a JavaScript file:\n\n${e.message}` 103 | ); 104 | break; 105 | } 106 | } 107 | fatal(e); 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /lib/cmds/scripts.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { common, serverArgs } = require("../util/cli"); 3 | const { detail, fatal, info, json } = require("../util/log"); 4 | const { ERROR_SERVICE_NOT_FOUND } = require("../errors"); 5 | 6 | const { white } = require("chalk"); 7 | const client = require("../util/client"); 8 | const { group } = require("../util/text"); 9 | const resolveServer = require("../resolveServer"); 10 | 11 | const command = (exports.command = "scripts "); 12 | exports.description = "List available scripts for a mounted service"; 13 | 14 | const describe = 15 | "Fetches a list of the scripts defined by the service. Returns an object mapping the raw script names to human-friendly names."; 16 | 17 | const args = [["mount", "Mount path of the service"]]; 18 | 19 | exports.builder = (yargs) => 20 | common(yargs, { command, describe, args }) 21 | .options({ 22 | ...serverArgs, 23 | raw: { 24 | describe: "Output raw JSON response", 25 | type: "boolean", 26 | default: false, 27 | }, 28 | }) 29 | .example( 30 | "$0 scripts /hello", 31 | 'Shows all scripts of the service at the URL "/hello"' 32 | ) 33 | .example( 34 | "$0 scripts /hello --server http://localhost:8530", 35 | "Use the server on port 8530 instead of the default" 36 | ) 37 | .example( 38 | "$0 scripts /hello --database mydb", 39 | 'Use the database "mydb" instead of the default' 40 | ); 41 | 42 | exports.handler = async function handler(argv) { 43 | try { 44 | const server = await resolveServer(argv); 45 | const db = client(server); 46 | const scripts = await db.listServiceScripts(argv.mount); 47 | const names = Object.keys(scripts); 48 | if (argv.raw) { 49 | json(scripts); 50 | } else if (names.length) { 51 | info(group(...names.map((name) => [name, scripts[name]]))); 52 | } else if (argv.verbose) { 53 | detail("No scripts available."); 54 | } 55 | } catch (e) { 56 | if (e.isArangoError && e.errorNum === ERROR_SERVICE_NOT_FOUND) { 57 | fatal(`No service found at "${white(argv.mount)}".`); 58 | } 59 | fatal(e); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /lib/cmds/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { common } = require("../util/cli"); 3 | 4 | const command = (exports.command = "server "); 5 | exports.description = "Manage ArangoDB server credentials"; 6 | const aliases = (exports.aliases = ["remote"]); 7 | 8 | const describe = `The server commands allow defining server aliases that can be used instead of URLs to avoid passing the same credentials to every command.`; 9 | 10 | exports.builder = (yargs) => 11 | common(yargs, { command, aliases, describe }) 12 | .command(require("./server/list")) 13 | .command(require("./server/remove")) 14 | .command(require("./server/set")) 15 | .command(require("./server/show")); 16 | -------------------------------------------------------------------------------- /lib/cmds/server/list.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { common } = require("../../util/cli"); 3 | const { info, fatal } = require("../../util/log"); 4 | const { group } = require("../../util/text"); 5 | const { load: loadIni } = require("../../ini"); 6 | 7 | const command = (exports.command = "list"); 8 | exports.description = "List known servers"; 9 | const aliases = (exports.aliases = ["ls"]); 10 | 11 | const describe = `List all known servers by their aliases.`; 12 | 13 | exports.builder = (yargs) => 14 | common(yargs, { command, sub: "server", aliases, describe }) 15 | .describe("verbose", "Include URLs") 16 | .example("$0 server list", "Shows all known servers") 17 | .example("$0 server list -v", "Shows all known servers and their URLs"); 18 | 19 | exports.handler = async function handler(argv) { 20 | try { 21 | const ini = await loadIni(); 22 | const servers = Object.keys(ini.server); 23 | if (!servers) return; 24 | if (argv.verbose) { 25 | info(group(...servers.map((name) => [name, ini.server[name].url]))); 26 | } else { 27 | for (const name of servers) { 28 | info(name); 29 | } 30 | } 31 | } catch (e) { 32 | fatal(e); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /lib/cmds/server/remove.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { common } = require("../../util/cli"); 3 | const { fatal } = require("../../util/log"); 4 | const { load: loadIni, save: saveIni } = require("../../ini"); 5 | 6 | const command = (exports.command = "remove "); 7 | exports.description = "Remove server"; 8 | const aliases = (exports.aliases = ["rm"]); 9 | 10 | const describe = "Removes a server from the list of known servers."; 11 | 12 | const args = [["name", "Server name to forget"]]; 13 | 14 | exports.builder = (yargs) => 15 | common(yargs, { command, sub: "server", aliases, describe, args }).example( 16 | "$0 server remove dev", 17 | 'Removes the server named "dev"' 18 | ); 19 | 20 | exports.handler = async function handler(argv) { 21 | try { 22 | const ini = await loadIni(); 23 | const servers = Object.keys(ini.server); 24 | if (!servers || !servers.includes(argv.name)) return; 25 | delete ini.server[argv.name]; 26 | return await saveIni(ini); 27 | } catch (e) { 28 | fatal(e); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /lib/cmds/server/set.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { common, serverArgs } = require("../../util/cli"); 3 | const { load: loadIni, save: saveIni } = require("../../ini"); 4 | 5 | const { fatal } = require("../../util/log"); 6 | const { omit } = require("lodash"); 7 | const resolveServer = require("../../resolveServer"); 8 | const { white } = require("chalk"); 9 | 10 | const command = (exports.command = "set "); 11 | exports.description = "Define server"; 12 | const aliases = (exports.aliases = ["add"]); 13 | 14 | const describe = 15 | "Defines a server under a given alias including its credentials."; 16 | 17 | const args = [ 18 | ["name", "Server name to define"], 19 | ["server", "URL of the ArangoDB server"], 20 | ]; 21 | 22 | exports.builder = (yargs) => 23 | common(yargs, { command, sub: "server", aliases, describe, args }) 24 | .options({ 25 | ...omit(serverArgs, ["server"]), 26 | }) 27 | .example( 28 | "$0 server set dev http://localhost:8529", 29 | 'Set the "dev" server to the ArangoDB instance at "http://localhost:8529" with the default username and password' 30 | ) 31 | .example( 32 | "$0 server set dev http://localhost:8529 -D mydb", 33 | 'Use the database "mydb" instead of "_system"' 34 | ) 35 | .example( 36 | "$0 server set dev http://localhost:8529 -u devel", 37 | 'Authenticate with user "devel" and an empty password' 38 | ) 39 | .example( 40 | "$0 server set dev http://localhost:8529 -u devel -P", 41 | 'Authenticate with user "devel" and a password read from stdin' 42 | ) 43 | .example( 44 | "$0 server set dev http://localhost:8529 -T", 45 | "Authenticate with a bearer token read from stdin" 46 | ) 47 | .example( 48 | "$0 server set dev http://devel:@mydbserver.example:8529", 49 | "Username and password can be passed via the URL (in this case the password is empty)" 50 | ) 51 | .example( 52 | "$0 server set dev tcp://localhost:8529", 53 | 'The protocol "tcp" can be used as an alias for "http"' 54 | ) 55 | .example( 56 | "$0 server set dev ssl://localhost:8529", 57 | 'The protocol "ssl" can be used as an alias for "https"' 58 | ) 59 | .example( 60 | "$0 server set dev //localhost:8529", 61 | 'If omitted the protocol defaults to "http"' 62 | ) 63 | .example( 64 | "$0 server set dev unix:///tmp/arangod.sock", 65 | 'Unix sockets work with the unix protocol instead of "http".' 66 | ) 67 | .example( 68 | "$0 server set dev https+unix:///tmp/arangod.sock", 69 | 'For HTTPS over unix sockets, just use the "https+unix" protocol.' 70 | ) 71 | .example( 72 | "$0 server set dev http://unix:/tmp/arangod.sock", 73 | "The conventional unix socket URL format is also supported." 74 | ) 75 | .example( 76 | "$0 server set dev http://localhost:8529 -V 3.2.0", 77 | "Explicitly setting the expected ArangoDB version can be useful when using servers running different versions" 78 | ); 79 | 80 | exports.handler = async function handler(argv) { 81 | if (argv.name.startsWith("/")) { 82 | fatal( 83 | `The server name must not start with a slash: "${white(argv.name)}".` 84 | ); 85 | } 86 | try { 87 | const server = await resolveServer(argv); 88 | const ini = await loadIni(); 89 | ini.server[argv.name] = omit(server, ["name"]); 90 | return await saveIni(ini); 91 | } catch (e) { 92 | fatal(e); 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /lib/cmds/server/show.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { bold, white, gray } = require("chalk"); 3 | const { common } = require("../../util/cli"); 4 | const { info, fatal } = require("../../util/log"); 5 | const { comma, inline: il } = require("../../util/text"); 6 | const { load: loadIni } = require("../../ini"); 7 | 8 | const command = (exports.command = "show "); 9 | exports.description = "Show server information"; 10 | const aliases = (exports.aliases = ["info"]); 11 | 12 | const describe = `Shows information about a server including its alias and URL.`; 13 | 14 | const args = [["name", "Server name to show details of"]]; 15 | 16 | exports.builder = (yargs) => 17 | common(yargs, { command, sub: "server", aliases, describe, args }) 18 | .describe("verbose", "Include passwords and tokens") 19 | .example( 20 | "$0 server show dev", 21 | 'Shows information about the server named "dev" not including password and token' 22 | ) 23 | .example( 24 | "$0 server show dev -v", 25 | 'Shows information about the server named "dev" including password and token' 26 | ); 27 | 28 | exports.handler = async function handler(argv) { 29 | try { 30 | const ini = await loadIni(); 31 | const servers = Object.keys(ini.server); 32 | if (!servers.length) { 33 | fatal("No servers defined."); 34 | } 35 | if (!servers.includes(argv.name)) { 36 | fatal(il` 37 | No such server: "${white(argv.name)}". 38 | Known servers: ${comma(servers.sort().map((name) => bold(name)))} 39 | `); 40 | } 41 | const server = ini.server[argv.name]; 42 | info(`URL: ${server.url}`); 43 | if (server.database !== undefined) { 44 | info(`Database: ${server.database}`); 45 | } 46 | if (server.version !== undefined) { 47 | info(`Version: ${server.version}`); 48 | } 49 | if (server.username !== undefined) { 50 | info(`Username: ${server.username}`); 51 | } 52 | if (argv.verbose) { 53 | if (server.password !== undefined) { 54 | info( 55 | `Password: ${server.password ? server.password : gray("(empty)")}` 56 | ); 57 | } 58 | if (server.token !== undefined) { 59 | info(`Token: ${server.token}`); 60 | } 61 | } else { 62 | if (server.password !== undefined) { 63 | info(`Password: ${gray("(hidden)")}`); 64 | } 65 | if (server.token !== undefined) { 66 | info(`Token: ${gray("(hidden)")}`); 67 | } 68 | } 69 | } catch (e) { 70 | fatal(e); 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /lib/cmds/set-dev.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { common, serverArgs } = require("../util/cli"); 3 | const { ERROR_SERVICE_NOT_FOUND } = require("../errors"); 4 | const { white } = require("chalk"); 5 | 6 | const client = require("../util/client"); 7 | const { fatal } = require("../util/log"); 8 | const { inline: il } = require("../util/text"); 9 | const resolveServer = require("../resolveServer"); 10 | 11 | const command = (exports.command = "set-dev "); 12 | exports.description = "Activate development mode for a mounted service"; 13 | const aliases = (exports.aliases = ["set-development"]); 14 | 15 | const describe = il`Puts the service into development mode. 16 | 17 | While the service is running in development mode the service will be reloaded from the filesystem and its setup script (if any) will be re-executed every time the service handles a request. 18 | 19 | When running ArangoDB in a cluster with multiple coordinators note that changes to the filesystem on one coordinator will not be reflected across the other coordinators. This means you should treat your coordinators as inconsistent as long as any service is running in development mode.`; 20 | 21 | const args = [["mount", "Mount path of the service"]]; 22 | 23 | exports.builder = (yargs) => 24 | common(yargs, { command, aliases, describe, args }) 25 | .options(serverArgs) 26 | .example( 27 | "$0 set-dev /hello", 28 | 'Activates the development mode for a Foxx service at the URL "/hello"' 29 | ) 30 | .example( 31 | "$0 set-dev --server http://locahost:8530 /hello", 32 | "Use the server on port 8530 instead of the default" 33 | ) 34 | .example( 35 | "$0 set-dev --database mydb /hello", 36 | 'Use the database "mydb" instead of the default' 37 | ) 38 | .example( 39 | "$0 set-dev --server dev /hello", 40 | 'Use the "dev" server instead of the default. See the "server" command for details' 41 | ); 42 | 43 | exports.handler = async function handler(argv) { 44 | try { 45 | const server = await resolveServer(argv); 46 | const db = client(server); 47 | return await db.setServiceDevelopmentMode(argv.mount, true); 48 | } catch (e) { 49 | if (e.isArangoError && e.errorNum === ERROR_SERVICE_NOT_FOUND) { 50 | fatal(`No service found at "${white(argv.mount)}".`); 51 | } 52 | fatal(e); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /lib/cmds/set-prod.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { common, serverArgs } = require("../util/cli"); 3 | const { ERROR_SERVICE_NOT_FOUND } = require("../errors"); 4 | const { white } = require("chalk"); 5 | 6 | const client = require("../util/client"); 7 | const { fatal } = require("../util/log"); 8 | const { inline: il } = require("../util/text"); 9 | const resolveServer = require("../resolveServer"); 10 | 11 | const command = (exports.command = "set-prod "); 12 | exports.description = "Disable development for a mounted service"; 13 | const aliases = (exports.aliases = ["set-production"]); 14 | 15 | const describe = il`Puts the service at the given mount path into production mode. 16 | 17 | When running ArangoDB in a cluster with multiple coordinators this will replace the service on all other coordinators with the version on this coordinator.`; 18 | 19 | const args = [["mount", "Mount path of the service"]]; 20 | 21 | exports.builder = (yargs) => 22 | common(yargs, { command, aliases, describe, args }) 23 | .options(serverArgs) 24 | .example( 25 | "$0 set-prod /hello", 26 | 'Disables the development mode for a Foxx service at the URL "/hello"' 27 | ) 28 | .example( 29 | "$0 set-prod --server http://locahost:8530 /hello", 30 | "Use the server on port 8530 instead of the default" 31 | ) 32 | .example( 33 | "$0 set-prod --database mydb /hello", 34 | 'Use the database "mydb" instead of the default' 35 | ) 36 | .example( 37 | "$0 set-prod --server dev /hello", 38 | 'Use the "dev" server instead of the default. See the "server" command for details' 39 | ); 40 | 41 | exports.handler = async function handler(argv) { 42 | try { 43 | const server = await resolveServer(argv); 44 | const db = client(server); 45 | return await db.setServiceDevelopmentMode(argv.mount, false); 46 | } catch (e) { 47 | if (e.isArangoError && e.errorNum === ERROR_SERVICE_NOT_FOUND) { 48 | fatal(`No service found at "${white(argv.mount)}".`); 49 | } 50 | fatal(e); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /lib/cmds/show.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { bold, gray, white } = require("chalk"); 3 | const { common, serverArgs } = require("../util/cli"); 4 | const { fatal, info, json } = require("../util/log"); 5 | 6 | const client = require("../util/client"); 7 | const { ERROR_SERVICE_NOT_FOUND } = require("../errors"); 8 | const resolveServer = require("../resolveServer"); 9 | 10 | const command = (exports.command = "show "); 11 | exports.description = "Show mounted service information"; 12 | const aliases = (exports.aliases = ["info"]); 13 | 14 | const describe = `Shows detailed information about the service installed at the given ${bold( 15 | "mount" 16 | )}.`; 17 | 18 | const args = [["mount", "Mount path of the service"]]; 19 | 20 | exports.builder = (yargs) => 21 | common(yargs, { command, aliases, describe, args }) 22 | .options({ 23 | ...serverArgs, 24 | raw: { 25 | describe: "Output raw JSON response", 26 | type: "boolean", 27 | default: false, 28 | }, 29 | }) 30 | .example( 31 | "$0 show /hello", 32 | 'Shows information about a Foxx service at the URL "/hello"' 33 | ) 34 | .example( 35 | "$0 show --server http://locahost:8530 /hello", 36 | "Use the server on port 8530 instead of the default" 37 | ) 38 | .example( 39 | "$0 show --database mydb /hello", 40 | 'Use the database "mydb" instead of the default' 41 | ) 42 | .example( 43 | "$0 show --server dev /hello", 44 | 'Use the "dev" server instead of the default. See the "server" command for details' 45 | ); 46 | 47 | exports.handler = async function handler(argv) { 48 | try { 49 | const server = await resolveServer(argv); 50 | const db = client(server); 51 | const result = await db.getService(argv.mount); 52 | if (argv.raw) { 53 | json(result); 54 | } else { 55 | const parts = ["Mount:", result.mount]; 56 | if (result.legacy) parts.push(gray("(legacy)")); 57 | if (result.development) parts.push(bold("[DEV]")); 58 | info(parts.join(" ")); 59 | if (result.name) info(`Name: ${result.name}`); 60 | if (result.version) info(`Version: ${result.version}`); 61 | info(`Path: ${result.path}`); 62 | info(`Checksum: ${result.checksum}`); 63 | } 64 | } catch (e) { 65 | if (e.isArangoError && e.errorNum === ERROR_SERVICE_NOT_FOUND) { 66 | fatal(`No service found at "${white(argv.mount)}".`); 67 | } 68 | fatal(e); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /lib/cmds/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { bold, white } = require("chalk"); 3 | const { common, serverArgs } = require("../util/cli"); 4 | const { fatal, info, json } = require("../util/log"); 5 | const { group, inline: il } = require("../util/text"); 6 | 7 | const client = require("../util/client"); 8 | const errors = require("../errors"); 9 | const reporters = require("../reporters"); 10 | const resolveServer = require("../resolveServer"); 11 | 12 | const command = (exports.command = "test [filter]"); 13 | exports.description = "Run the tests of a mounted service"; 14 | const aliases = (exports.aliases = ["tests", "run-tests"]); 15 | 16 | const describe = 17 | il` 18 | Run the tests of a mounted service. 19 | 20 | Output is controlled with the ${bold("--reporter")} option: 21 | ` + 22 | "\n\n" + 23 | group( 24 | ["spec", "Hierarchical specification of nested test cases", "[default]"], 25 | ["list", "Simple list of test cases"], 26 | ["min", "Just the summary and failures"], 27 | ["json", "Single large raw JSON object"], 28 | ["tap", "Output suitable for Test-Anything-Protocol consumers"], 29 | [ 30 | "stream", 31 | 'Line-delimited JSON stream of "events" beginning with a single "start", followed by "pass" or "fail" for each test and ending with a single "end"', 32 | ], 33 | ["xunit", "Jenkins-compatible xUnit-style XML output"] 34 | ); 35 | 36 | const args = [ 37 | ["mount", "Mount path of the service"], 38 | ["filter", "Only run tests with full names matching this string"], 39 | ]; 40 | 41 | exports.builder = (yargs) => 42 | common(yargs, { command, aliases, describe, args }) 43 | .options({ 44 | ...serverArgs, 45 | reporter: { 46 | describe: "Reporter to use for result data", 47 | alias: "R", 48 | choices: ["spec", "list", "min", "json", "tap", "stream", "xunit"], 49 | default: "spec", 50 | }, 51 | }) 52 | .example( 53 | "$0 test /hello", 54 | 'Runs the tests of a Foxx service at the URL "/hello"' 55 | ) 56 | .example( 57 | "$0 test -R json /hello", 58 | "Use the json reporter instead of the default" 59 | ) 60 | .example( 61 | "$0 test --server http://locahost:8530 /hello", 62 | "Use the server on port 8530 instead of the default" 63 | ) 64 | .example( 65 | "$0 test --database mydb /hello", 66 | 'Use the database "mydb" instead of the default' 67 | ) 68 | .example( 69 | "$0 test --server dev /hello", 70 | 'Use the "dev" server instead of the default. See the "server" command for details' 71 | ); 72 | 73 | exports.handler = async function handler(argv) { 74 | try { 75 | const server = await resolveServer(argv); 76 | return await runTests(server, argv.mount, argv.reporter, argv.filter); 77 | } catch (e) { 78 | fatal(e); 79 | } 80 | }; 81 | 82 | async function runTests(server, mount, cliReporter, filter) { 83 | const db = client(server); 84 | let apiReporter; 85 | if (cliReporter === "spec") apiReporter = "suite"; 86 | else if (cliReporter === "json") apiReporter = "default"; 87 | else if (cliReporter === "list") apiReporter = "default"; 88 | else if (cliReporter === "min") apiReporter = "default"; 89 | else apiReporter = cliReporter; 90 | 91 | let result; 92 | try { 93 | result = await db.runServiceTests(mount, { reporter: apiReporter, filter }); 94 | } catch (e) { 95 | if (e.isArangoError) { 96 | switch (e.errorNum) { 97 | case errors.ERROR_SERVICE_NOT_FOUND: 98 | fatal(`No service found at "${white(mount)}".`); 99 | break; 100 | case errors.ERROR_SERVICE_NEEDS_CONFIGURATION: 101 | fatal( 102 | `Service at "${white( 103 | mount 104 | )}" is missing configuration or dependencies.` 105 | ); 106 | break; 107 | case errors.ERROR_MODULE_NOT_FOUND: 108 | fatal( 109 | `Server encountered errors trying to locate a JavaScript file:\n\n${e.message}\n\nMake sure the service bundle includes all files referenced in the manifest.` 110 | ); 111 | break; 112 | case errors.ERROR_MODULE_FAILURE: 113 | fatal( 114 | `Server encountered errors executing a JavaScript file:\n\n${e.message}\n\nMake sure all tests are specified via the manifest, not loaded directly from another test file. For details check the arangod server logs.` 115 | ); 116 | break; 117 | case errors.ERROR_MODULE_SYNTAX_ERROR: 118 | fatal( 119 | `Server encountered errors trying to parse a JavaScript file:\n\n${e.message}` 120 | ); 121 | break; 122 | } 123 | } 124 | throw e; 125 | } 126 | 127 | if (cliReporter === "xunit") { 128 | info(result); 129 | const lines = result.split("\n"); 130 | const match = lines[1].match(/ failures="(\d+)"/); 131 | process.exit((match && Number(match[1])) || 0); 132 | } 133 | 134 | if (cliReporter === "tap") { 135 | info(result); 136 | const lines = result.split("\n"); 137 | while (lines.length > 1 && !lines[lines.length - 1]) lines.pop(); 138 | const match = lines[lines.length - 1].match(/# fail (\d+)/); 139 | process.exit((match && Number(match[1])) || 0); 140 | } 141 | 142 | if (cliReporter === "stream") { 143 | info(result); 144 | const lines = result.split("\n"); 145 | process.exit(lines.filter((line) => line.startsWith('["fail",')).length); 146 | } 147 | 148 | if (cliReporter === "list" || cliReporter === "min") { 149 | const failures = reporters.list(result, cliReporter === "min"); 150 | process.exit(failures || 0); 151 | } 152 | 153 | if (cliReporter === "spec") { 154 | const failures = reporters.suite(result); 155 | process.exit(failures || 0); 156 | } 157 | 158 | if (typeof result === "string") { 159 | info(result); 160 | } else { 161 | json(result); 162 | } 163 | process.exit( 164 | result && result.stats && typeof result.stats.failures === "number" 165 | ? result.stats.failures 166 | : 0 167 | ); 168 | } 169 | -------------------------------------------------------------------------------- /lib/cmds/uninstall.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { common, serverArgs } = require("../util/cli"); 3 | const { detail, fatal } = require("../util/log"); 4 | 5 | const { bold } = require("chalk"); 6 | const client = require("../util/client"); 7 | const { ERROR_SERVICE_NOT_FOUND } = require("../errors"); 8 | const resolveServer = require("../resolveServer"); 9 | 10 | const command = (exports.command = "uninstall "); 11 | exports.description = "Uninstall a mounted service"; 12 | const aliases = (exports.aliases = ["remove", "purge"]); 13 | 14 | const describe = `Removes the service at the given ${bold( 15 | "mount" 16 | )} path from the database and file system.`; 17 | 18 | const args = [["mount", "Mount path of the service"]]; 19 | 20 | exports.builder = (yargs) => 21 | common(yargs, { command, aliases, describe, args }) 22 | .options({ 23 | ...serverArgs, 24 | teardown: { 25 | describe: `Run the teardown script before uninstalling the service. Use ${bold( 26 | "--no-teardown" 27 | )} to disable`, 28 | type: "boolean", 29 | default: true, 30 | }, 31 | }) 32 | .example( 33 | "$0 uninstall /hello", 34 | 'Uninstalls a Foxx service at the URL "/hello"' 35 | ) 36 | .example( 37 | "$0 uninstall --no-teardown /hello", 38 | "Does not run the teardown script before uninstalling" 39 | ) 40 | .example( 41 | "$0 uninstall --server http://locahost:8530 /hello", 42 | "Use the server on port 8530 instead of the default" 43 | ) 44 | .example( 45 | "$0 uninstall --database mydb /hello", 46 | 'Use the database "mydb" instead of the default' 47 | ) 48 | .example( 49 | "$0 uninstall --server dev /hello", 50 | 'Use the "dev" server instead of the default. See the "server" command for details' 51 | ); 52 | 53 | exports.handler = async function handler(argv) { 54 | try { 55 | const server = await resolveServer(argv); 56 | const db = client(server); 57 | try { 58 | await db.uninstallService(argv.mount, { teardown: argv.teardown }); 59 | } catch (e) { 60 | if (e.isArangoError && e.errorNum === ERROR_SERVICE_NOT_FOUND) { 61 | if (argv.verbose) { 62 | detail(`Service "${argv.mount}" not found.\nNothing to uninstall.`); 63 | } 64 | process.exit(0); 65 | } 66 | throw e; 67 | } 68 | if (argv.verbose) { 69 | detail(`Service "${argv.mount}" successfully removed.`); 70 | } 71 | } catch (e) { 72 | fatal(e); 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.ERROR_MALFORMED_MANIFEST_FILE = 3000; 3 | exports.ERROR_INVALID_SERVICE_MANIFEST = 3001; 4 | exports.ERROR_INVALID_FOXX_OPTIONS = 3004; 5 | exports.ERROR_INVALID_MOUNTPOINT = 3007; 6 | exports.ERROR_SERVICE_NOT_FOUND = 3009; 7 | exports.ERROR_SERVICE_NEEDS_CONFIGURATION = 3010; 8 | exports.ERROR_SERVICE_MOUNTPOINT_CONFLICT = 3011; 9 | exports.ERROR_SERVICE_MANIFEST_NOT_FOUND = 3012; 10 | exports.ERROR_SERVICE_OPTIONS_MALFORMED = 3013; 11 | exports.ERROR_SERVICE_SOURCE_NOT_FOUND = 3014; 12 | exports.ERROR_SERVICE_SOURCE_ERROR = 3015; 13 | exports.ERROR_SERVICE_UNKNOWN_SCRIPT = 3016; 14 | exports.ERROR_MODULE_NOT_FOUND = 3100; 15 | exports.ERROR_MODULE_SYNTAX_ERROR = 3101; 16 | exports.ERROR_MODULE_FAILURE = 3103; 17 | -------------------------------------------------------------------------------- /lib/generator/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { render } = require("ejs"); 3 | const { join } = require("path"); 4 | const { readFileSync } = require("fs"); 5 | const inflect = require("i")(); 6 | 7 | const TEMPLATE_PATH = join(__dirname, "..", "..", "templates"); 8 | 9 | function generateManifest(options) { 10 | const manifest = { 11 | $schema: "http://json.schemastore.org/foxx-manifest", 12 | main: options.mainFile, 13 | engines: { 14 | arangodb: options.engineVersion, 15 | }, 16 | }; 17 | 18 | if (options.name) manifest.name = options.name; 19 | if (options.version) manifest.version = options.version; 20 | if (options.license) manifest.license = options.license; 21 | if (options.authorEmail) { 22 | manifest.author = `${options.authorName} <${options.authorEmail}>`; 23 | } else if (options.authorName) manifest.author = options.authorName; 24 | 25 | if (options.description) manifest.description = options.description; 26 | if (options.configuration) manifest.configuration = options.configuration; 27 | if (options.dependencies) manifest.dependencies = options.dependencies; 28 | if (options.provides) manifest.provides = options.provides; 29 | 30 | if ( 31 | (options.documentCollections && options.documentCollections.length) || 32 | (options.edgeCollections && options.edgeCollections.length) 33 | ) { 34 | manifest.scripts = {}; 35 | manifest.scripts.setup = "scripts/setup.js"; 36 | manifest.scripts.teardown = "scripts/teardown.js"; 37 | } 38 | if (options.tests) manifest.tests = options.tests; 39 | 40 | return JSON.stringify(manifest, null, 2); 41 | } 42 | 43 | async function generateFile(name, data) { 44 | const template = readFileSync(join(TEMPLATE_PATH, `${name}.ejs`), "utf-8"); 45 | return render(template, data); 46 | } 47 | 48 | async function generateLicense(options) { 49 | if (!options.license) return generateFile("LICENSE", options); 50 | return require(`spdx-license-list/licenses/${options.license}.json`) 51 | .licenseText.replace(/<]+>>/g, "") 52 | .replace(/<>/g, ""); 53 | } 54 | 55 | exports.generateFiles = async (options) => { 56 | const files = []; 57 | files.push({ 58 | name: "manifest.json", 59 | content: generateManifest(options), 60 | }); 61 | files.push({ 62 | name: "index.js", 63 | content: await generateFile( 64 | options.example ? "example/index.js" : "index.js", 65 | options 66 | ), 67 | }); 68 | files.push({ 69 | name: "README.md", 70 | content: await generateFile("README.md", options), 71 | }); 72 | if (options.license) { 73 | files.push({ 74 | name: "LICENSE", 75 | content: await generateLicense(options), 76 | }); 77 | } 78 | const collections = []; 79 | if (options.documentCollections) { 80 | for (const collection of options.documentCollections) { 81 | collections.push([collection, false]); 82 | } 83 | } 84 | if (options.edgeCollections) { 85 | for (const collection of options.edgeCollections) { 86 | collections.push([collection, true]); 87 | } 88 | } 89 | if (options.generateCrudRoutes) { 90 | for (const [collection, isEdgeCollection] of collections) { 91 | files.push({ 92 | name: `api/${collection}.js`, 93 | content: await exports.generateCrud(collection, isEdgeCollection), 94 | }); 95 | } 96 | } 97 | if (collections.length) { 98 | files.push({ 99 | name: "scripts/setup.js", 100 | content: await generateFile("setup.js", options), 101 | }); 102 | files.push({ 103 | name: "scripts/teardown.js", 104 | content: await generateFile("teardown.js", options), 105 | }); 106 | } 107 | 108 | return files; 109 | }; 110 | 111 | exports.generateCrud = async ( 112 | collection, 113 | isEdgeCollection, 114 | prefixed = true 115 | ) => { 116 | let singular = inflect.singularize(collection); 117 | if (singular === collection) singular += "Item"; 118 | let plural = inflect.pluralize(singular); 119 | if (plural === singular) plural = collection; 120 | return await generateFile("crud.js", { 121 | collection, 122 | isEdgeCollection, 123 | singular, 124 | plural, 125 | prefixed, 126 | }); 127 | }; 128 | 129 | exports.generateScript = async () => await generateFile("script.js", {}); 130 | 131 | exports.generateRouter = async () => await generateFile("router.js", {}); 132 | 133 | exports.generateIndex = async () => await generateFile("index.js", {}); 134 | 135 | exports.generateTest = async () => await generateFile("test.js", {}); 136 | -------------------------------------------------------------------------------- /lib/ignore.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { exists, readFile, writeFile } = require("./util/fs"); 3 | 4 | const { Minimatch } = require("minimatch"); 5 | 6 | const defaults = [".git/", ".svn/", ".hg/", "*.swp", ".DS_Store"]; 7 | 8 | exports.load = async function load(file) { 9 | let lines = defaults; 10 | if (await exists(file)) { 11 | const text = await readFile(file, "utf-8"); 12 | lines = text.replace(/\r/g, "").split(/\n+/g); 13 | } 14 | return exports.buildMatcher(lines); 15 | }; 16 | 17 | exports.buildMatcher = function buildMatcher(lines) { 18 | const blacklist = []; 19 | const whitelist = []; 20 | for (const line of lines) { 21 | let list = blacklist; 22 | let pattern = line.trim(); 23 | if (pattern.startsWith("!")) { 24 | list = whitelist; 25 | pattern = pattern.slice(1); 26 | } 27 | if (!pattern) continue; 28 | if (pattern.endsWith("/")) pattern += "**"; 29 | if (!pattern.startsWith("/")) pattern = "**/" + pattern; 30 | else pattern = pattern.slice(1); 31 | list.push(new Minimatch(pattern, { dot: true, nonegate: true })); 32 | } 33 | return (path) => 34 | whitelist.every((matcher) => !matcher.match(path)) && 35 | blacklist.some((matcher) => matcher.match(path)); 36 | }; 37 | 38 | exports.save = async function save(file, values, overwrite) { 39 | const patterns = new Set(values); 40 | if (!overwrite) { 41 | if (await exists(file)) { 42 | const text = await readFile(file, "utf-8"); 43 | for (const line of text.split(/\n|\r/g)) { 44 | if (!line) continue; 45 | patterns.add(line); 46 | } 47 | } else { 48 | for (const line of defaults) { 49 | patterns.add(line); 50 | } 51 | } 52 | } 53 | const lines = Array.from(patterns.values()); 54 | await writeFile(file, lines.join("\n") + "\n"); 55 | }; 56 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { green } = require("chalk"); 3 | const yargs = require("yargs"); 4 | const { common } = require("./util/cli"); 5 | 6 | const foxx = green( 7 | ` 8 | /\\ 9 | (~( 10 | ) ) /\\_/\\ 11 | (_ -----_(@ @) 12 | ( \\ / 13 | /|/--\\|\\ V 14 | " " " "' 15 | `.slice(1, -1) 16 | ); 17 | 18 | common(yargs, { command: "", describe: foxx }) 19 | .wrap(Math.min(160, yargs.terminalWidth())) 20 | .help("help", "Show this usage information and exit") 21 | .command(require("./cmds/add")) 22 | .command(require("./cmds/bundle")) 23 | .command(require("./cmds/config")) 24 | .command(require("./cmds/deps")) 25 | .command(require("./cmds/download")) 26 | .command(require("./cmds/ignore")) 27 | .command(require("./cmds/init")) 28 | .command(require("./cmds/install")) 29 | .command(require("./cmds/list")) 30 | .command(require("./cmds/replace")) 31 | .command(require("./cmds/run")) 32 | .command(require("./cmds/scripts")) 33 | .command(require("./cmds/server")) 34 | .command(require("./cmds/set-dev")) 35 | .command(require("./cmds/set-prod")) 36 | .command(require("./cmds/show")) 37 | .command(require("./cmds/test")) 38 | .command(require("./cmds/uninstall")) 39 | .command(require("./cmds/upgrade")) 40 | .recommendCommands() 41 | .options({ 42 | version: { 43 | describe: "Show version information and exit", 44 | alias: "V", 45 | type: "boolean", 46 | default: false, 47 | }, 48 | verbose: { 49 | describe: "More output", 50 | alias: "v", 51 | type: "count", 52 | }, 53 | }) 54 | .global("verbose") 55 | .group(["version", "help", "verbose"], "General options:"); 56 | 57 | module.exports = yargs; 58 | -------------------------------------------------------------------------------- /lib/ini.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { homedir } = require("os"); 3 | const { resolve } = require("path"); 4 | const { encode, decode } = require("ini"); 5 | const { exists, readFile, writeFile } = require("./util/fs"); 6 | 7 | const RC_FILENAME = ".foxxrc"; 8 | 9 | function getRcFilePath() { 10 | return process.env.FOXXRC_PATH || resolve(homedir(), RC_FILENAME); 11 | } 12 | 13 | exports.load = async function load() { 14 | const defaults = { 15 | server: {}, 16 | }; 17 | const rcfile = getRcFilePath(); 18 | if (!(await exists(rcfile))) { 19 | return defaults; 20 | } 21 | const data = await readFile(rcfile, "utf-8"); 22 | const obj = decode(data); 23 | return Object.assign(defaults, obj); 24 | }; 25 | 26 | exports.save = async function save(obj) { 27 | const rcfile = getRcFilePath(); 28 | const data = encode(obj); 29 | await writeFile(rcfile, data); 30 | }; 31 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { version } = require("../package.json"); 3 | const { info } = require("./util/log"); 4 | const yargs = require("."); 5 | 6 | const argv = yargs.argv; 7 | if (!argv._.length) { 8 | if (argv.version) { 9 | info(version); 10 | process.exit(0); 11 | } else { 12 | yargs.showHelp(); 13 | process.exit(1); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/reporters.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { red, green, cyan, gray } = require("chalk"); 3 | const { indentable: indentableLog } = require("./util/log"); 4 | 5 | exports.list = function list(result, minimal = false) { 6 | const logger = indentableLog(1); 7 | const errors = []; 8 | logger.log(); 9 | if (minimal) { 10 | for (const test of result.tests) { 11 | if (test.err.stack) { 12 | errors.push({ stack: test.err.stack, fullTitle: test.fullTitle }); 13 | } 14 | } 15 | } else { 16 | for (const test of result.tests) { 17 | if (test.err.stack) { 18 | errors.push({ stack: test.err.stack, fullTitle: test.fullTitle }); 19 | logger.log(red(`${errors.length}) ${test.fullTitle}`)); 20 | } else if (typeof test.duration !== "number") { 21 | logger.log(cyan("-"), cyan(test.fullTitle)); 22 | } else { 23 | logger.log( 24 | green("✓"), 25 | gray(`${test.fullTitle}:`), 26 | gray(`${test.duration}ms`) 27 | ); 28 | } 29 | } 30 | logger.log(); 31 | } 32 | printSummaryAndErrors(result.stats, logger, errors); 33 | logger.log(); 34 | return errors.length; 35 | }; 36 | 37 | exports.suite = function suite(result) { 38 | const logger = indentableLog(1); 39 | const errors = []; 40 | logger.log(); 41 | printSuite(result, logger, errors, []); 42 | logger.log(); 43 | printSummaryAndErrors(result.stats, logger, errors); 44 | logger.log(); 45 | return errors.length; 46 | }; 47 | 48 | function printSuite(suite, logger, errors, title) { 49 | if (suite.title) logger.log(suite.title); 50 | if (!suite.stats) logger.indent(); 51 | for (const test of suite.tests) { 52 | if (test.err.stack) { 53 | errors.push({ 54 | stack: test.err.stack, 55 | fullTitle: [...title, test.title].join(" "), 56 | }); 57 | logger.log(red(`${errors.length}) ${test.title}`)); 58 | } else if (typeof test.duration !== "number") { 59 | logger.log(cyan("-"), cyan(test.title)); 60 | } else { 61 | logger.log(green("✓"), gray(test.title), gray(`(${test.duration}ms)`)); 62 | } 63 | } 64 | if (suite.stats && suite.tests.length && suite.suites.length) logger.log(); 65 | for (const child of suite.suites) { 66 | printSuite( 67 | child, 68 | logger, 69 | errors, 70 | suite.title ? [...title, suite.title] : title 71 | ); 72 | if (suite.stats) logger.log(); 73 | } 74 | if (!suite.stats) logger.dedent(); 75 | } 76 | 77 | function printSummaryAndErrors(stats, logger, errors) { 78 | if (stats.passes || !errors.length) { 79 | logger.log(green(`${stats.passes} passing`), gray(`(${stats.duration}ms)`)); 80 | } 81 | if (stats.pending) { 82 | logger.log(cyan(`${stats.pending} pending`)); 83 | } 84 | if (errors.length) { 85 | logger.log(red(`${stats.failures} failing`)); 86 | logger.log(); 87 | for (let i = 0; i < errors.length; i++) { 88 | const error = errors[i]; 89 | logger.log(`${i + 1}) ${error.fullTitle}`); 90 | const [message, ...stack] = error.stack.split("\n"); 91 | logger.indent(); 92 | logger.log(red(message)); 93 | logger.indent(); 94 | for (const line of stack) { 95 | logger.log(gray(line.trimLeft())); 96 | } 97 | logger.dedent(2); 98 | logger.log(); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/resolveServer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { bold, white } = require("chalk"); 3 | const { fatal } = require("./util/log"); 4 | const { load: loadIni } = require("./ini"); 5 | const { prompt } = require("inquirer"); 6 | const { unsplat } = require("./util/array"); 7 | const { readFile } = require("./util/fs"); 8 | const { parse: parseUrl } = require("url"); 9 | 10 | async function resolve(endpointOrName = "default") { 11 | if (endpointOrName.match(/^\/\//)) { 12 | return { url: `http:${endpointOrName}` }; 13 | } 14 | if (endpointOrName.match(/^(unix|(https?|tcp|ssl|tls)(\+unix)?):\/\//)) { 15 | return { url: endpointOrName }; 16 | } 17 | const ini = await loadIni(); 18 | if (ini.server[endpointOrName]) { 19 | return { 20 | ...ini.server[endpointOrName], 21 | name: endpointOrName, 22 | }; 23 | } 24 | if (endpointOrName === "default") { 25 | return { name: endpointOrName }; 26 | } 27 | return null; 28 | } 29 | 30 | module.exports = async function resolveServer(argv) { 31 | if (argv.password && argv.token) { 32 | fatal( 33 | `Can not use both ${bold("password")} and ${bold( 34 | "token" 35 | )} as authentication for the same server.` 36 | ); 37 | } 38 | if (argv.passwordFile && argv.token) { 39 | fatal( 40 | `Can not use both ${bold("passwordFile")} and ${bold( 41 | "token" 42 | )} as authentication for the same server.` 43 | ); 44 | } 45 | if (argv.passwordFile && argv.password) { 46 | fatal( 47 | `Can not use both ${bold("passwordFile")} and ${bold( 48 | "password" 49 | )} as authentication for the same server.` 50 | ); 51 | } 52 | if (argv.username && argv.token) { 53 | fatal( 54 | `Can not use both ${bold("username")} and ${bold( 55 | "token" 56 | )} as authentication for the same server.` 57 | ); 58 | } 59 | const server = await resolve(unsplat(argv.server)); 60 | if (!server) { 61 | fatal(`Not a valid server: "${white(argv.server)}".`); 62 | } 63 | if (server.url === undefined) { 64 | server.url = "http://localhost:8529"; 65 | } 66 | if (argv.database) { 67 | server.database = unsplat(argv.database); 68 | } else if (server.database === undefined) { 69 | server.database = "_system"; 70 | } 71 | if (argv.username) { 72 | delete server.token; 73 | server.username = unsplat(argv.username); 74 | server.password = ""; 75 | } 76 | if (argv.passwordFile) { 77 | delete server.token; 78 | try { 79 | server.password = await readFile(argv.passwordFile, "utf-8"); 80 | } catch (e) { 81 | fatal(`Error reading password file "${white(argv.passwordFile)}".`); 82 | } 83 | } 84 | if (argv.password) { 85 | delete server.token; 86 | const { password } = await prompt([ 87 | { 88 | message: "Password", 89 | name: "password", 90 | type: "password", 91 | }, 92 | ]); 93 | server.password = password; 94 | } 95 | if (argv.token) { 96 | delete server.username; 97 | delete server.password; 98 | const { token } = await prompt([ 99 | { 100 | message: "Token", 101 | name: "token", 102 | type: "password", 103 | validate: Boolean, 104 | }, 105 | ]); 106 | server.token = token; 107 | } 108 | if (server.token === undefined && !parseUrl(server.url).auth) { 109 | if (server.username === undefined) { 110 | server.username = "root"; 111 | } 112 | if (server.password === undefined) { 113 | server.password = ""; 114 | } 115 | } 116 | if (server.password) { 117 | server.password = server.password.replace(/\r|\n/g, ""); 118 | } 119 | return server; 120 | }; 121 | -------------------------------------------------------------------------------- /lib/resolveToStream.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { createBundle } = require("./bundle"); 3 | const { createReadStream } = require("fs"); 4 | const { fatal } = require("./util/log"); 5 | const got = require("got"); 6 | const { parse: parseUrl } = require("url"); 7 | const { safeStat } = require("./util/fs"); 8 | 9 | module.exports = async function resolveToStream(path) { 10 | if (path === "@") { 11 | const stream = process.stdin; 12 | stream.path = "data.bin"; 13 | return stream; 14 | } 15 | const stats = await safeStat(path); 16 | if (stats) { 17 | if (stats.isDirectory(path)) { 18 | return bundleToStream(path); 19 | } 20 | return createReadStream(path); 21 | } 22 | const { protocol } = parseUrl(path); 23 | if (protocol) { 24 | return await downloadToBuffer(path); 25 | } 26 | fatal(`No such file or directory: "${path}".`); 27 | }; 28 | 29 | async function downloadToBuffer(path) { 30 | try { 31 | const res = await got(path, { responseType: "buffer" }); 32 | if (res.statusCode >= 400) { 33 | fatal( 34 | `Server responded with code ${res.statusCode} while fetching "${path}".` 35 | ); 36 | } 37 | return res.body; 38 | } catch (e) { 39 | fatal(`Failed to resolve URL "${path}".`); 40 | } 41 | } 42 | 43 | async function bundleToStream(path) { 44 | const temppath = await createBundle(path); 45 | return createReadStream(temppath); 46 | } 47 | -------------------------------------------------------------------------------- /lib/test/add-crud.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const foxxUtil = require("./util"); 6 | const expect = require("chai").expect; 7 | const os = require("os"); 8 | const fs = require("fs"); 9 | const rmDir = require("./fs").rmDir; 10 | const tmpDir = path.resolve(os.tmpdir(), "test-init-service"); 11 | const foxx = (command) => foxxUtil(command, false, { cwd: tmpDir }); 12 | 13 | const checkFileEqual = (file, content) => { 14 | const filePath = path.resolve(tmpDir, file); 15 | expect(fs.existsSync(filePath)).to.equal(true); 16 | expect(fs.readFileSync(filePath, "utf-8").replace(/\r/g, "")).to.equal( 17 | content 18 | ); 19 | }; 20 | 21 | const checkFileContains = (file, content) => { 22 | const filePath = path.resolve(tmpDir, file); 23 | expect(fs.existsSync(filePath)).to.equal(true); 24 | expect(fs.readFileSync(filePath, "utf-8").replace(/\r/g, "")).contains( 25 | content 26 | ); 27 | }; 28 | 29 | describe("Foxx service add crud", () => { 30 | beforeEach(async () => { 31 | if (fs.existsSync(tmpDir)) { 32 | try { 33 | rmDir(tmpDir); 34 | } catch (e) { 35 | // noop 36 | } 37 | } 38 | await foxxUtil(`init ${tmpDir}`); 39 | }); 40 | 41 | it("should create the route file and add it to the index.js", async () => { 42 | await foxx(`add crud hello`); 43 | expect(fs.existsSync(path.resolve(tmpDir, "api", "hello.js"))); 44 | checkFileEqual( 45 | "index.js", 46 | "'use strict';\n\nmodule.context.use('/hello', require('./api/hello'), 'hello');\n" 47 | ); 48 | }); 49 | 50 | it("should use module.context.collection in router", async () => { 51 | await foxx(`add crud hello`); 52 | checkFileContains("api/hello.js", "module.context.collection"); 53 | }); 54 | 55 | it("with option unprefixed should use db._collection in router", async () => { 56 | await foxx(`add crud hello --unprefixed`); 57 | checkFileContains("api/hello.js", "const db = require('@arangodb').db;"); 58 | checkFileContains("api/hello.js", "db._collection"); 59 | }); 60 | 61 | it("with option unprefixed (alias) should use db._collection in router", async () => { 62 | await foxx(`add crud hello -u`); 63 | checkFileContains("api/hello.js", "const db = require('@arangodb').db;"); 64 | checkFileContains("api/hello.js", "db._collection"); 65 | }); 66 | 67 | it("with option edge should use edge schema in router", async () => { 68 | await foxx(`add crud hello --edge`); 69 | checkFileContains("api/hello.js", "_from: joi.string()"); 70 | checkFileContains("api/hello.js", "_to: joi.string()"); 71 | }); 72 | 73 | it("with option edge (alias) should use edge schema in router", async () => { 74 | await foxx(`add crud hello -e`); 75 | checkFileContains("api/hello.js", "_from: joi.string()"); 76 | checkFileContains("api/hello.js", "_to: joi.string()"); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /lib/test/add-router.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const foxxUtil = require("./util"); 6 | const expect = require("chai").expect; 7 | const os = require("os"); 8 | const fs = require("fs"); 9 | const rmDir = require("./fs").rmDir; 10 | const tmpDir = path.resolve(os.tmpdir(), "test-init-service"); 11 | const foxx = (command) => foxxUtil(command, false, { cwd: tmpDir }); 12 | 13 | const checkFile = (file, content) => { 14 | const filePath = path.resolve(tmpDir, file); 15 | expect(fs.existsSync(filePath)).to.equal(true); 16 | expect(fs.readFileSync(filePath, "utf-8").replace(/\r/g, "")).to.equal( 17 | content 18 | ); 19 | }; 20 | 21 | describe("Foxx service add router", () => { 22 | beforeEach(async () => { 23 | if (fs.existsSync(tmpDir)) { 24 | try { 25 | rmDir(tmpDir); 26 | } catch (e) { 27 | // noop 28 | } 29 | } 30 | await foxxUtil(`init ${tmpDir}`); 31 | }); 32 | 33 | it("should create the route file and add it to the index.js", async () => { 34 | await foxx(`add router hello`); 35 | expect(fs.existsSync(path.resolve(tmpDir, "api", "hello.js"))); 36 | checkFile( 37 | "index.js", 38 | "'use strict';\n\nmodule.context.use('/hello', require('./api/hello'), 'hello');\n" 39 | ); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /lib/test/add-script.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const foxxUtil = require("./util"); 6 | const expect = require("chai").expect; 7 | const os = require("os"); 8 | const fs = require("fs"); 9 | const rmDir = require("./fs").rmDir; 10 | const tmpDir = path.resolve(os.tmpdir(), "test-init-service"); 11 | const foxx = (command) => foxxUtil(command, false, { cwd: tmpDir }); 12 | 13 | const checkFile = (file, content) => { 14 | const filePath = path.resolve(tmpDir, file); 15 | expect(fs.existsSync(filePath)).to.equal(true); 16 | expect(fs.readFileSync(filePath, "utf-8").replace(/\r/g, "")).to.equal( 17 | content 18 | ); 19 | }; 20 | 21 | describe("Foxx service add script", () => { 22 | beforeEach(async () => { 23 | if (fs.existsSync(tmpDir)) { 24 | try { 25 | rmDir(tmpDir); 26 | } catch (e) { 27 | // noop 28 | } 29 | } 30 | await foxxUtil(`init ${tmpDir}`); 31 | }); 32 | 33 | it("should create the script file and add it to the manifest", async () => { 34 | await foxx(`add script hello`); 35 | checkFile( 36 | "scripts/hello.js", 37 | "'use strict';\nconst db = require('@arangodb').db;\nconst args = module.context.argv;\n\n// module.exports = \"script result\";\n" 38 | ); 39 | const manifest = JSON.parse( 40 | fs.readFileSync(path.resolve(tmpDir, "manifest.json"), "utf-8") 41 | ); 42 | expect(manifest).to.have.property("scripts"); 43 | expect(manifest.scripts).to.have.property("hello", "scripts/hello.js"); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /lib/test/add-test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const foxxUtil = require("./util"); 6 | const expect = require("chai").expect; 7 | const os = require("os"); 8 | const fs = require("fs"); 9 | const rmDir = require("./fs").rmDir; 10 | const tmpDir = path.resolve(os.tmpdir(), "test-init-service"); 11 | const foxx = (command) => foxxUtil(command, false, { cwd: tmpDir }); 12 | 13 | const checkFile = (file, content) => { 14 | const filePath = path.resolve(tmpDir, file); 15 | expect(fs.existsSync(filePath)).to.equal(true); 16 | expect(fs.readFileSync(filePath, "utf-8").replace(/\r/g, "")).to.equal( 17 | content 18 | ); 19 | }; 20 | 21 | describe("Foxx service add test", () => { 22 | beforeEach(async () => { 23 | if (fs.existsSync(tmpDir)) { 24 | try { 25 | rmDir(tmpDir); 26 | } catch (e) { 27 | // noop 28 | } 29 | } 30 | await foxxUtil(`init ${tmpDir}`); 31 | }); 32 | 33 | it("should create the test Javascript file", async () => { 34 | await foxx(`add test hello`); 35 | checkFile( 36 | "test/hello.js", 37 | "/*global describe, it, before, after, beforeEach, afterEach */\n'use strict';\nconst expect = require('chai').expect;\n\ndescribe('test suite', () => {\n it('contains a test case', () => {\n expect(true).not.to.equal(false);\n });\n});\n" 38 | ); 39 | }); 40 | 41 | describe("with missing property tests in manifest.json", () => { 42 | const manifestPath = path.resolve(tmpDir, "manifest.json"); 43 | 44 | beforeEach(async () => { 45 | const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")); 46 | manifest.tests = undefined; 47 | fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); 48 | }); 49 | 50 | it("should add tests pattern to manifest.json", async () => { 51 | let manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")); 52 | expect(manifest).to.not.have.property("tests"); 53 | await foxx(`add test hello`); 54 | manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")); 55 | expect(manifest).to.have.property("tests", "test/**/*.js"); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /lib/test/bundle.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const foxx = require("./util"); 6 | const expect = require("chai").expect; 7 | const os = require("os"); 8 | const fs = require("fs"); 9 | 10 | const basePath = path.resolve(__dirname, "..", "..", "fixtures"); 11 | const tmpFile = path.resolve(os.tmpdir(), "minimal-working-service.zip"); 12 | 13 | describe("Foxx service bundle", () => { 14 | beforeEach(async () => { 15 | if (fs.existsSync(tmpFile)) { 16 | try { 17 | fs.unlinkSync(tmpFile); 18 | } catch (e) { 19 | // noop 20 | } 21 | } 22 | }); 23 | 24 | it("should output bundle per default", async () => { 25 | const output = await foxx( 26 | `bundle ${path.resolve(basePath, "minimal-working-service")}` 27 | ); 28 | expect(output).to.match(/^PK\u0003\u0004/); 29 | }); 30 | 31 | it("via alias should output bundle per default", async () => { 32 | const output = await foxx( 33 | `zip ${path.resolve(basePath, "minimal-working-service")}` 34 | ); 35 | expect(output).to.match(/^PK\u0003\u0004/); 36 | }); 37 | 38 | it("should output bundle with option stdout", async () => { 39 | const output = await foxx( 40 | `bundle --stdout ${path.resolve(basePath, "minimal-working-service")}` 41 | ); 42 | expect(output).to.match(/^PK\u0003\u0004/); 43 | }); 44 | 45 | it("should output bundle with alias of option stdout", async () => { 46 | const output = await foxx( 47 | `bundle -O ${path.resolve(basePath, "minimal-working-service")}` 48 | ); 49 | expect(output).to.match(/^PK\u0003\u0004/); 50 | }); 51 | 52 | it("should write bundle to outfile", async () => { 53 | const output = await foxx( 54 | `bundle --outfile ${tmpFile} ${path.resolve( 55 | basePath, 56 | "minimal-working-service" 57 | )}` 58 | ); 59 | expect(output).to.equal(""); 60 | expect(fs.existsSync(tmpFile)).to.equal(true); 61 | expect(fs.readFileSync(tmpFile, "utf-8")).to.match(/^PK\u0003\u0004/); 62 | }); 63 | 64 | it("via alias should write bundle to outfile", async () => { 65 | const output = await foxx( 66 | `bundle -o ${tmpFile} ${path.resolve( 67 | basePath, 68 | "minimal-working-service" 69 | )}` 70 | ); 71 | expect(output).to.equal(""); 72 | expect(fs.existsSync(tmpFile)).to.equal(true); 73 | expect(fs.readFileSync(tmpFile, "utf-8")).to.match(/^PK\u0003\u0004/); 74 | }); 75 | 76 | it("should not overwrite outfile per default", async () => { 77 | fs.writeFileSync(tmpFile, "no"); 78 | try { 79 | await foxx( 80 | `bundle --outfile ${tmpFile} ${path.resolve( 81 | basePath, 82 | "minimal-working-service" 83 | )}` 84 | ); 85 | } catch (e) { 86 | expect(fs.existsSync(tmpFile)).to.equal(true); 87 | expect(fs.readFileSync(tmpFile, "utf-8")).to.equal("no"); 88 | return; 89 | } 90 | expect.fail(); 91 | }); 92 | 93 | it("should overwrite outfile when forced", async () => { 94 | fs.writeFileSync(tmpFile, ""); 95 | const output = await foxx( 96 | `bundle --outfile ${tmpFile} --force ${path.resolve( 97 | basePath, 98 | "minimal-working-service" 99 | )}` 100 | ); 101 | expect(output).to.equal(""); 102 | expect(fs.existsSync(tmpFile)).to.equal(true); 103 | expect(fs.readFileSync(tmpFile, "utf-8")).to.match(/^PK\u0003\u0004/); 104 | }); 105 | 106 | it("should overwrite outfile when forced via alias", async () => { 107 | fs.writeFileSync(tmpFile, ""); 108 | const output = await foxx( 109 | `bundle -o ${tmpFile} -f ${path.resolve( 110 | basePath, 111 | "minimal-working-service" 112 | )}` 113 | ); 114 | expect(output).to.equal(""); 115 | expect(fs.existsSync(tmpFile)).to.equal(true); 116 | expect(fs.readFileSync(tmpFile, "utf-8")).to.match(/^PK\u0003\u0004/); 117 | }); 118 | 119 | it("should refuse when missing manifest", async () => { 120 | try { 121 | await foxx(`bundle ${path.resolve(basePath, "sloppy-service")}`); 122 | } catch (e) { 123 | return; 124 | } 125 | expect.fail(); 126 | }); 127 | 128 | it("should refuse when missing manifest even if forced", async () => { 129 | try { 130 | await foxx(`bundle -f ${path.resolve(basePath, "sloppy-service")}`); 131 | } catch (e) { 132 | return; 133 | } 134 | expect.fail(); 135 | }); 136 | 137 | it("should bundle even if missing manifest when sloppy", async () => { 138 | const output = await foxx( 139 | `bundle --sloppy ${path.resolve(basePath, "sloppy-service")}` 140 | ); 141 | expect(output).to.match(/^PK\u0003\u0004/); 142 | }); 143 | 144 | it("should not bundle if source does not exist", async () => { 145 | try { 146 | await foxx(`bundle ${path.resolve(basePath, "no-such-service")}`); 147 | } catch (e) { 148 | return; 149 | } 150 | expect.fail(); 151 | }); 152 | 153 | it("should output bundle of cwd", async () => { 154 | const output = await foxx("bundle", false, { 155 | cwd: path.resolve(basePath, "minimal-working-service"), 156 | }); 157 | expect(output).to.match(/^PK\u0003\u0004/); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /lib/test/fs/index.js: -------------------------------------------------------------------------------- 1 | `use strict`; 2 | 3 | const fs = require("fs"); 4 | 5 | module.exports.rmDir = (path) => { 6 | if (fs.existsSync(path)) { 7 | const files = fs.readdirSync(path); 8 | for (const file of files) { 9 | const current = `${path}/${file}`; 10 | if (fs.lstatSync(current).isDirectory()) { 11 | exports.rmDir(current); 12 | } else { 13 | fs.unlinkSync(current); 14 | } 15 | } 16 | fs.rmdirSync(path); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /lib/test/helper/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const basePath = path.resolve(__dirname, "..", "..", "..", "fixtures"); 5 | 6 | module.exports.crudCases = () => { 7 | return [ 8 | { 9 | name: "localJsFile", 10 | source: () => path.resolve(basePath, "minimal-working-service.js"), 11 | }, 12 | { 13 | name: "localZipFile", 14 | source: () => path.resolve(basePath, "minimal-working-service.zip"), 15 | }, 16 | { 17 | name: "localDir", 18 | source: () => path.resolve(basePath, "minimal-working-service"), 19 | }, 20 | { 21 | name: "localDirWithSymlink", 22 | source: () => path.resolve(basePath, "symlink-service"), 23 | }, 24 | { 25 | name: "remoteJsFile", 26 | source: (arangoPaths) => `--remote ${arangoPaths.local.js}`, 27 | }, 28 | { 29 | name: "remoteZipFile", 30 | source: (arangoPaths) => `--remote ${arangoPaths.local.zip}`, 31 | }, 32 | { 33 | name: "remoteDir", 34 | source: (arangoPaths) => `--remote ${arangoPaths.local.dir}`, 35 | }, 36 | { 37 | name: "remoteShortJsFile", 38 | source: (arangoPaths) => `-R ${arangoPaths.local.js}`, 39 | }, 40 | { 41 | name: "remoteShortZipFile", 42 | source: (arangoPaths) => `-R ${arangoPaths.local.zip}`, 43 | }, 44 | { 45 | name: "remoteShortDir", 46 | source: (arangoPaths) => `-R ${arangoPaths.local.dir}`, 47 | }, 48 | { 49 | name: "localJsURL", 50 | source: (arangoPaths) => arangoPaths.remote.js, 51 | }, 52 | { 53 | name: "localZipURL", 54 | source: (arangoPaths) => arangoPaths.remote.zip, 55 | }, 56 | ]; 57 | }; 58 | -------------------------------------------------------------------------------- /lib/test/ignore.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const foxxUtil = require("./util"); 6 | const expect = require("chai").expect; 7 | const os = require("os"); 8 | const fs = require("fs"); 9 | const rmDir = require("./fs").rmDir; 10 | 11 | const tmpDir = path.resolve(os.tmpdir(), "foxx-ignore-test"); 12 | const ignoreFile = path.resolve(tmpDir, ".foxxignore"); 13 | 14 | const foxx = (command) => foxxUtil(command, false, { cwd: tmpDir }); 15 | const defaults = `.git/ 16 | .svn/ 17 | .hg/ 18 | *.swp 19 | .DS_Store 20 | `; 21 | 22 | describe("Foxx ignore", () => { 23 | beforeEach(async () => { 24 | if (fs.existsSync(tmpDir)) { 25 | rmDir(tmpDir); 26 | } 27 | fs.mkdirSync(tmpDir); 28 | }); 29 | 30 | it("without params should create default ignore file", async () => { 31 | await foxx("ignore"); 32 | expect(fs.existsSync(ignoreFile)).to.equal(true); 33 | expect(fs.readFileSync(ignoreFile, "utf-8")).to.equal(defaults); 34 | }); 35 | 36 | it("via alias without params should create default ignore file", async () => { 37 | await foxx("exclude"); 38 | expect(fs.existsSync(ignoreFile)).to.equal(true); 39 | expect(fs.readFileSync(ignoreFile, "utf-8")).to.equal(defaults); 40 | }); 41 | 42 | it("with param first time called should create ignore file", async () => { 43 | await foxx("ignore test"); 44 | expect(fs.existsSync(ignoreFile)).to.equal(true); 45 | const content = fs.readFileSync(ignoreFile, "utf-8"); 46 | expect(content).to.have.string(defaults); 47 | expect(content).to.have.string("test"); 48 | }); 49 | 50 | it("via alias with param first time called should create ignore file", async () => { 51 | await foxx("exclude test"); 52 | expect(fs.existsSync(ignoreFile)).to.equal(true); 53 | const content = fs.readFileSync(ignoreFile, "utf-8"); 54 | expect(content).to.have.string(defaults); 55 | expect(content).to.have.string("test"); 56 | }); 57 | 58 | it("called with multiple params should include every param", async () => { 59 | await foxx("ignore test1 test2"); 60 | const content = fs.readFileSync(ignoreFile, "utf-8"); 61 | expect(content).to.have.string(defaults); 62 | expect(content).to.have.string("test1"); 63 | expect(content).to.have.string("test2"); 64 | }); 65 | 66 | it("called a second time should not overwrite previous pattern", async () => { 67 | await foxx("ignore test1"); 68 | await foxx("ignore test2"); 69 | const content = fs.readFileSync(ignoreFile, "utf-8"); 70 | expect(content).to.have.string(defaults); 71 | expect(content).to.have.string("test1"); 72 | expect(content).to.have.string("test2"); 73 | }); 74 | 75 | it("with option force should overwrite defaults", async () => { 76 | await foxx("ignore test1 test2 --force"); 77 | const content = fs.readFileSync(ignoreFile, "utf-8"); 78 | expect(content).to.not.have.string(defaults); 79 | expect(content).to.have.string("test1"); 80 | expect(content).to.have.string("test2"); 81 | }); 82 | 83 | it("with option force should overwrite previous pattern", async () => { 84 | await foxx("ignore test1"); 85 | await foxx("ignore test2 --force"); 86 | const content = fs.readFileSync(ignoreFile, "utf-8"); 87 | expect(content).to.not.have.string(defaults); 88 | expect(content).to.not.have.string("test1"); 89 | expect(content).to.have.string("test2"); 90 | }); 91 | 92 | it("should be considered when creating a bundle", async () => { 93 | fs.writeFileSync(path.resolve(tmpDir, "test1"), ""); 94 | fs.writeFileSync(path.resolve(tmpDir, "test2"), ""); 95 | fs.writeFileSync(path.resolve(tmpDir, "manifest.json"), "{}"); 96 | await foxx("ignore test1"); 97 | const tmpFile = path.resolve(tmpDir, "bundle.zip"); 98 | await foxx(`bundle --outfile ${tmpFile}`); 99 | await require("../util/fs").extract(tmpFile, { 100 | dir: path.resolve(tmpDir, "bundle"), 101 | }); 102 | expect(fs.existsSync(path.resolve(tmpDir, "bundle", "test1"))).to.equal( 103 | false 104 | ); 105 | expect(fs.existsSync(path.resolve(tmpDir, "bundle", "test2"))).to.equal( 106 | true 107 | ); 108 | expect( 109 | fs.existsSync(path.resolve(tmpDir, "bundle", ".foxxignore")) 110 | ).to.equal(true); 111 | }); 112 | 113 | it("non-existing should be considered when creating a bundle", async () => { 114 | fs.mkdirSync(path.resolve(tmpDir, ".git")); 115 | fs.writeFileSync(path.resolve(tmpDir, ".git", "test"), ""); 116 | fs.writeFileSync(path.resolve(tmpDir, "manifest.json"), "{}"); 117 | const tmpFile = path.resolve(tmpDir, "bundle.zip"); 118 | await foxx(`bundle --outfile ${tmpFile}`); 119 | await require("../util/fs").extract(tmpFile, { 120 | dir: path.resolve(tmpDir, "bundle"), 121 | }); 122 | expect(fs.existsSync(path.resolve(tmpDir, "bundle", ".git"))).to.equal( 123 | false 124 | ); 125 | }); 126 | 127 | it("defaults should be considered when creating a bundle", async () => { 128 | fs.mkdirSync(path.resolve(tmpDir, ".git")); 129 | fs.writeFileSync(path.resolve(tmpDir, ".git", "test"), ""); 130 | fs.writeFileSync(path.resolve(tmpDir, "manifest.json"), "{}"); 131 | await foxx("ignore"); 132 | const tmpFile = path.resolve(tmpDir, "bundle.zip"); 133 | await foxx(`bundle --outfile ${tmpFile}`); 134 | await require("../util/fs").extract(tmpFile, { 135 | dir: path.resolve(tmpDir, "bundle"), 136 | }); 137 | expect(fs.existsSync(path.resolve(tmpDir, "bundle", ".git"))).to.equal( 138 | false 139 | ); 140 | }); 141 | 142 | it("empty should be considered when creating a bundle", async () => { 143 | fs.mkdirSync(path.resolve(tmpDir, ".git")); 144 | fs.writeFileSync(path.resolve(tmpDir, ".git", "test"), ""); 145 | fs.writeFileSync(path.resolve(tmpDir, "manifest.json"), "{}"); 146 | await foxx("ignore -f"); 147 | const tmpFile = path.resolve(tmpDir, "bundle.zip"); 148 | await foxx(`bundle --outfile ${tmpFile}`); 149 | await require("../util/fs").extract(tmpFile, { 150 | dir: path.resolve(tmpDir, "bundle"), 151 | }); 152 | expect(fs.existsSync(path.resolve(tmpDir, "bundle", ".git"))).to.equal( 153 | true 154 | ); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /lib/test/init.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const foxx = require("./util"); 6 | const expect = require("chai").expect; 7 | const os = require("os"); 8 | const fs = require("fs"); 9 | const rmDir = require("./fs").rmDir; 10 | 11 | const tmpDir = path.resolve(os.tmpdir(), "test-init-service"); 12 | 13 | const checkFile = (file, content) => { 14 | expect( 15 | fs.readFileSync(path.resolve(tmpDir, file), "utf-8").replace(/\r/g, "") 16 | ).to.equal(content); 17 | }; 18 | 19 | describe("Foxx service init", () => { 20 | describe("called with an non-existing directory", () => { 21 | beforeEach(async () => { 22 | if (fs.existsSync(tmpDir)) { 23 | try { 24 | rmDir(tmpDir); 25 | } catch (e) { 26 | // noop 27 | } 28 | } 29 | }); 30 | 31 | it("should create the document", async () => { 32 | await foxx(`init ${tmpDir}`); 33 | expect(fs.existsSync(tmpDir)).to.equal(true); 34 | }); 35 | }); 36 | 37 | describe("called with an existing directory", () => { 38 | beforeEach(async () => { 39 | if (fs.existsSync(tmpDir)) { 40 | try { 41 | rmDir(tmpDir); 42 | } catch (e) { 43 | // noop 44 | } 45 | fs.mkdirSync(tmpDir); 46 | } 47 | }); 48 | 49 | it("should create service files", async () => { 50 | await foxx(`init ${tmpDir}`); 51 | expect(fs.existsSync(tmpDir)).to.equal(true); 52 | const files = fs.readdirSync(tmpDir); 53 | expect(files).contain("manifest.json"); 54 | expect(files).contain("index.js"); 55 | expect(files).contain("README.md"); 56 | expect(files).contain("api"); 57 | expect(files).contain("scripts"); 58 | expect(files).contain("test"); 59 | 60 | checkFile("index.js", "'use strict';\n\n"); 61 | checkFile( 62 | "README.md", 63 | "# test-init-service\n\n## License\n\nCopyright (c) " + 64 | new Date().getFullYear() + 65 | " . All rights reserved.\n" 66 | ); 67 | expect(fs.readdirSync(path.resolve(tmpDir, "api"))).to.be.empty; 68 | expect(fs.readdirSync(path.resolve(tmpDir, "scripts"))).to.be.empty; 69 | expect(fs.readdirSync(path.resolve(tmpDir, "test"))).to.be.empty; 70 | const manifest = JSON.parse( 71 | fs.readFileSync(path.resolve(tmpDir, "manifest.json"), "utf-8") 72 | ); 73 | expect(manifest).to.have.property( 74 | "$schema", 75 | "http://json.schemastore.org/foxx-manifest" 76 | ); 77 | expect(manifest).to.have.property("name", "test-init-service"); 78 | expect(manifest).to.have.property("main", "index.js"); 79 | expect(manifest).to.have.property("version", "0.0.0"); 80 | expect(manifest).to.have.property("tests", "test/**/*.js"); 81 | expect(manifest).to.have.property("engines"); 82 | expect(manifest.engines).to.have.property("arangodb", "^3.0.0"); 83 | }); 84 | 85 | it("with example option should create an example service", async () => { 86 | await foxx(`init ${tmpDir} --example`); 87 | expect(fs.existsSync(tmpDir)).to.equal(true); 88 | const files = fs.readdirSync(tmpDir); 89 | expect(files).contain("manifest.json"); 90 | expect(files).contain("index.js"); 91 | expect(files).contain("README.md"); 92 | expect(files).contain("api"); 93 | expect(files).contain("scripts"); 94 | expect(files).contain("test"); 95 | 96 | checkFile( 97 | "index.js", 98 | "'use strict';\nconst createRouter = require('@arangodb/foxx/router');\n\nconst router = createRouter();\nmodule.context.use(router);\n\nrouter.get('/', (req, res) => {\n res.write('Hello World!')\n})\n.response(['text/plain']);\n" 99 | ); 100 | checkFile( 101 | "README.md", 102 | "# hello-world\n\nA simple Hello World Foxx service\n\n## License\n\nThe Apache-2.0 license. For more information, see the accompanying LICENSE file.\n" 103 | ); 104 | expect(fs.readdirSync(path.resolve(tmpDir, "api"))).to.be.empty; 105 | expect(fs.readdirSync(path.resolve(tmpDir, "scripts"))).to.be.empty; 106 | expect(fs.readdirSync(path.resolve(tmpDir, "test"))).to.be.empty; 107 | const manifest = JSON.parse( 108 | fs.readFileSync(path.resolve(tmpDir, "manifest.json"), "utf-8") 109 | ); 110 | expect(manifest).to.have.property("name", "hello-world"); 111 | expect(manifest).to.have.property("main", "index.js"); 112 | expect(manifest).to.have.property("version", "0.0.0"); 113 | expect(manifest).to.have.property("tests", "test/**/*.js"); 114 | expect(manifest).to.have.property("engines"); 115 | expect(manifest.engines).to.have.property("arangodb", "^3.0.0"); 116 | expect(manifest).to.have.property("author", "ArangoDB GmbH"); 117 | }); 118 | 119 | it("with example (alias) option should create an example service", async () => { 120 | await foxx(`init ${tmpDir} -e`); 121 | expect(fs.existsSync(tmpDir)).to.equal(true); 122 | const files = fs.readdirSync(tmpDir); 123 | expect(files).contain("manifest.json"); 124 | expect(files).contain("index.js"); 125 | expect(files).contain("README.md"); 126 | expect(files).contain("api"); 127 | expect(files).contain("scripts"); 128 | expect(files).contain("test"); 129 | 130 | checkFile( 131 | "index.js", 132 | "'use strict';\nconst createRouter = require('@arangodb/foxx/router');\n\nconst router = createRouter();\nmodule.context.use(router);\n\nrouter.get('/', (req, res) => {\n res.write('Hello World!')\n})\n.response(['text/plain']);\n" 133 | ); 134 | checkFile( 135 | "README.md", 136 | "# hello-world\n\nA simple Hello World Foxx service\n\n## License\n\nThe Apache-2.0 license. For more information, see the accompanying LICENSE file.\n" 137 | ); 138 | expect(fs.readdirSync(path.resolve(tmpDir, "api"))).to.be.empty; 139 | expect(fs.readdirSync(path.resolve(tmpDir, "scripts"))).to.be.empty; 140 | expect(fs.readdirSync(path.resolve(tmpDir, "test"))).to.be.empty; 141 | const manifest = JSON.parse( 142 | fs.readFileSync(path.resolve(tmpDir, "manifest.json"), "utf-8") 143 | ); 144 | expect(manifest).to.have.property("name", "hello-world"); 145 | expect(manifest).to.have.property("main", "index.js"); 146 | expect(manifest).to.have.property("version", "0.0.0"); 147 | expect(manifest).to.have.property("tests", "test/**/*.js"); 148 | expect(manifest).to.have.property("engines"); 149 | expect(manifest.engines).to.have.property("arangodb", "^3.0.0"); 150 | expect(manifest).to.have.property("author", "ArangoDB GmbH"); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /lib/test/run.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, after */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const { Database } = require("arangojs"); 6 | const foxx = require("./util"); 7 | const expect = require("chai").expect; 8 | const fs = require("fs"); 9 | 10 | const ARANGO_VERSION = Number(process.env.ARANGO_VERSION || 30000); 11 | const ARANGO_URL = process.env.TEST_ARANGODB_URL || "http://localhost:8529"; 12 | const ARANGO_USERNAME = process.env.ARANGO_USERNAME || "root"; 13 | 14 | const mount = "/run-test"; 15 | const basePath = path.resolve(__dirname, "..", "..", "fixtures"); 16 | 17 | describe("Foxx service run", () => { 18 | const db = new Database({ 19 | url: ARANGO_URL, 20 | arangoVersion: ARANGO_VERSION, 21 | }); 22 | 23 | before(async () => { 24 | await db.installService( 25 | mount, 26 | fs.readFileSync(path.resolve(basePath, "echo-script.zip")) 27 | ); 28 | }); 29 | 30 | after(async () => { 31 | try { 32 | await db.uninstallService(mount, { force: true }); 33 | } catch (e) { 34 | // noop 35 | } 36 | }); 37 | 38 | it("should pass argv (empty object) to script and return exports", async () => { 39 | const resp = await foxx(`run ${mount} echo {}`); 40 | expect(JSON.parse(resp)).to.eql([{}]); 41 | }); 42 | 43 | it("should pass argv to script and return exports", async () => { 44 | const resp = await foxx(`run ${mount} echo {"hello":"world"}`); 45 | expect(JSON.parse(resp)).to.eql([{ hello: "world" }]); 46 | }); 47 | 48 | it("should treat array script argv like any other script argv", async () => { 49 | const resp = await foxx(`run ${mount} echo ["yes","please"]`); 50 | expect(JSON.parse(resp)).to.eql([["yes", "please"]]); 51 | }); 52 | 53 | it("via alias should pass argv to script and return exports", async () => { 54 | const resp = await foxx(`script ${mount} echo {}`); 55 | expect(JSON.parse(resp)).to.eql([{}]); 56 | }); 57 | 58 | it("non-existing script should not be available", async () => { 59 | try { 60 | await foxx(`run ${mount} no`); 61 | } catch (e) { 62 | return; 63 | } 64 | expect.fail(); 65 | }); 66 | 67 | it("with alternative server URL should pass argv", async () => { 68 | const resp = await foxx(`run ${mount} echo {} --server ${ARANGO_URL}`); 69 | expect(JSON.parse(resp)).to.eql([{}]); 70 | }); 71 | 72 | it("with alternative server URL (short option) should pass argv", async () => { 73 | const resp = await foxx(`run ${mount} echo {} -H ${ARANGO_URL}`); 74 | expect(JSON.parse(resp)).to.eql([{}]); 75 | }); 76 | 77 | it("with alternative database should pass argv", async () => { 78 | const resp = await foxx(`run ${mount} echo {} --database _system`); 79 | expect(JSON.parse(resp)).to.eql([{}]); 80 | }); 81 | 82 | it("with alternative database (short option) should pass argv", async () => { 83 | const resp = await foxx(`run ${mount} echo {} -D _system`); 84 | expect(JSON.parse(resp)).to.eql([{}]); 85 | }); 86 | 87 | it("with alternative username should pass argv", async () => { 88 | const resp = await foxx( 89 | `run ${mount} echo {} --username ${ARANGO_USERNAME}` 90 | ); 91 | expect(JSON.parse(resp)).to.eql([{}]); 92 | }); 93 | 94 | it("with alternative username should pass argv (short option)", async () => { 95 | const resp = await foxx(`run ${mount} echo {} -u ${ARANGO_USERNAME}`); 96 | expect(JSON.parse(resp)).to.eql([{}]); 97 | }); 98 | 99 | describe("with a password file", () => { 100 | const user = "testuser"; 101 | const passwordFilePath = path.resolve(basePath, "passwordFile"); 102 | const passwd = fs.readFileSync(passwordFilePath, "utf-8"); 103 | before(async () => { 104 | db.route("/_api/user").post({ 105 | user, 106 | passwd, 107 | }); 108 | db.route(`/_api/user/${user}/database/_system`).put({ grant: "rw" }); 109 | }); 110 | after(async () => { 111 | try { 112 | db.route(`/_api/user/${user}`).delete(); 113 | } catch (e) { 114 | // noop 115 | } 116 | }); 117 | it("should pass argv", async () => { 118 | const resp = await foxx( 119 | `run ${mount} echo {} --username ${user} --password-file ${passwordFilePath}` 120 | ); 121 | expect(JSON.parse(resp)).to.eql([{}]); 122 | }); 123 | }); 124 | 125 | it("should fail when mount is invalid", async () => { 126 | try { 127 | await foxx(`run /dev/null echo`); 128 | } catch (e) { 129 | return; 130 | } 131 | expect.fail(); 132 | }); 133 | 134 | it("should pass argv to script via stdin and return exports", async () => { 135 | const input = '{"hello":"world"}'; 136 | const resp = await foxx(`run ${mount} echo @`, false, { input }); 137 | expect(JSON.parse(resp)).to.eql([{ hello: "world" }]); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /lib/test/scripts.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, after */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const { Database } = require("arangojs"); 6 | const foxx = require("./util"); 7 | const expect = require("chai").expect; 8 | const fs = require("fs"); 9 | 10 | const ARANGO_VERSION = Number(process.env.ARANGO_VERSION || 30000); 11 | const ARANGO_URL = process.env.TEST_ARANGODB_URL || "http://localhost:8529"; 12 | const ARANGO_USERNAME = process.env.ARANGO_USERNAME || "root"; 13 | 14 | const mount = "/scripts-test"; 15 | const basePath = path.resolve(__dirname, "..", "..", "fixtures"); 16 | 17 | describe("Foxx service scripts", () => { 18 | const db = new Database({ 19 | url: ARANGO_URL, 20 | arangoVersion: ARANGO_VERSION, 21 | }); 22 | 23 | before(async () => { 24 | await db.installService( 25 | mount, 26 | fs.readFileSync( 27 | path.resolve(basePath, "minimal-working-setup-teardown.zip") 28 | ) 29 | ); 30 | }); 31 | 32 | after(async () => { 33 | try { 34 | await db.uninstallService(mount, { force: true }); 35 | } catch (e) { 36 | // noop 37 | } 38 | }); 39 | 40 | it("should all be listed", async () => { 41 | const scripts = await foxx(`scripts ${mount}`, true); 42 | expect(scripts).to.have.property("setup", "Setup"); 43 | expect(scripts).to.have.property("teardown", "Teardown"); 44 | }); 45 | 46 | it("with alternative server URL should all be listed", async () => { 47 | const scripts = await foxx(`scripts ${mount} --server ${ARANGO_URL}`, true); 48 | expect(scripts).to.have.property("setup", "Setup"); 49 | expect(scripts).to.have.property("teardown", "Teardown"); 50 | }); 51 | 52 | it("with alternative server URL (short option) should all be listed", async () => { 53 | const scripts = await foxx(`scripts ${mount} -H ${ARANGO_URL}`, true); 54 | expect(scripts).to.have.property("setup", "Setup"); 55 | expect(scripts).to.have.property("teardown", "Teardown"); 56 | }); 57 | 58 | it("with alternative database should all be listed", async () => { 59 | const scripts = await foxx(`scripts ${mount} --database _system`, true); 60 | expect(scripts).to.have.property("setup", "Setup"); 61 | expect(scripts).to.have.property("teardown", "Teardown"); 62 | }); 63 | 64 | it("with alternative database (short option) should all be listed", async () => { 65 | const scripts = await foxx(`scripts ${mount} -D _system`, true); 66 | expect(scripts).to.have.property("setup", "Setup"); 67 | expect(scripts).to.have.property("teardown", "Teardown"); 68 | }); 69 | 70 | it("with alternative username should all be listed", async () => { 71 | const scripts = await foxx( 72 | `scripts ${mount} --username ${ARANGO_USERNAME}`, 73 | true 74 | ); 75 | expect(scripts).to.have.property("setup", "Setup"); 76 | expect(scripts).to.have.property("teardown", "Teardown"); 77 | }); 78 | 79 | it("with alternative username should all be listed (short option)", async () => { 80 | const scripts = await foxx(`scripts ${mount} -u ${ARANGO_USERNAME}`, true); 81 | expect(scripts).to.have.property("setup", "Setup"); 82 | expect(scripts).to.have.property("teardown", "Teardown"); 83 | }); 84 | 85 | describe("with a password file", () => { 86 | const user = "testuser"; 87 | const passwordFilePath = path.resolve(basePath, "passwordFile"); 88 | const passwd = fs.readFileSync(passwordFilePath, "utf-8"); 89 | before(async () => { 90 | db.route("/_api/user").post({ 91 | user, 92 | passwd, 93 | }); 94 | db.route(`/_api/user/${user}/database/_system`).put({ grant: "rw" }); 95 | }); 96 | after(async () => { 97 | try { 98 | db.route(`/_api/user/${user}`).delete(); 99 | } catch (e) { 100 | // noop 101 | } 102 | }); 103 | it("should all be listed", async () => { 104 | const scripts = await foxx( 105 | `scripts ${mount} --username ${user} --password-file ${passwordFilePath}`, 106 | true 107 | ); 108 | expect(scripts).to.have.property("setup", "Setup"); 109 | expect(scripts).to.have.property("teardown", "Teardown"); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /lib/test/server-list.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const foxx = require("./util"); 6 | const expect = require("chai").expect; 7 | const os = require("os"); 8 | const fs = require("fs"); 9 | 10 | const foxxRcFile = path.resolve(os.tmpdir(), ".foxxrc"); 11 | 12 | describe("Foxx server list", () => { 13 | before(async () => { 14 | if (fs.existsSync(foxxRcFile)) { 15 | fs.unlinkSync(foxxRcFile); 16 | } 17 | await foxx("server set test1 //localhost:8529"); 18 | await foxx("server set test2 //localhost:8530"); 19 | }); 20 | 21 | it("should include added server", async () => { 22 | const server = await foxx("server list"); 23 | expect(server).to.equal("test1\ntest2\n"); 24 | }); 25 | 26 | it("via alias should include added server", async () => { 27 | const server = await foxx("remote ls"); 28 | expect(server).to.equal("test1\ntest2\n"); 29 | }); 30 | 31 | it("verbose should include added server with URLs", async () => { 32 | const server = await foxx("server list --verbose"); 33 | expect(server).to.equal( 34 | " test1 http://localhost:8529\n test2 http://localhost:8530\n" 35 | ); 36 | }); 37 | 38 | it("verbose via alias should include added server with URLs", async () => { 39 | const server = await foxx("server list -v"); 40 | expect(server).to.equal( 41 | " test1 http://localhost:8529\n test2 http://localhost:8530\n" 42 | ); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /lib/test/server-remove.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const foxx = require("./util"); 6 | const expect = require("chai").expect; 7 | const os = require("os"); 8 | const fs = require("fs"); 9 | 10 | const foxxRcFile = path.resolve(os.tmpdir(), ".foxxrc"); 11 | 12 | describe("Foxx server remove", () => { 13 | beforeEach(async () => { 14 | if (fs.existsSync(foxxRcFile)) { 15 | fs.unlinkSync(foxxRcFile); 16 | } 17 | await foxx("server set test1 //localhost:8529"); 18 | await foxx("server set test2 //localhost:8530"); 19 | }); 20 | 21 | it("should show added server", async () => { 22 | await foxx("server remove test1"); 23 | const content1 = fs.readFileSync(foxxRcFile, "utf-8"); 24 | expect(content1.replace(/\r\n/g, "\n")).to.equal( 25 | "[server.test2]\nurl=http://localhost:8530\ndatabase=_system\nusername=root\npassword=\n" 26 | ); 27 | await foxx("server remove test2"); 28 | const content2 = fs.readFileSync(foxxRcFile, "utf-8"); 29 | expect(content2).to.equal(""); 30 | }); 31 | 32 | it("via alias should show added server", async () => { 33 | await foxx("remote rm test1"); 34 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 35 | expect(content.replace(/\r\n/g, "\n")).to.equal( 36 | "[server.test2]\nurl=http://localhost:8530\ndatabase=_system\nusername=root\npassword=\n" 37 | ); 38 | }); 39 | 40 | it("verbose should show added server with password", async () => { 41 | await foxx("server remove test1 --verbose"); 42 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 43 | expect(content.replace(/\r\n/g, "\n")).to.equal( 44 | "[server.test2]\nurl=http://localhost:8530\ndatabase=_system\nusername=root\npassword=\n" 45 | ); 46 | }); 47 | 48 | it("verbose via alias should show added server with password", async () => { 49 | await foxx("server remove test1 -v"); 50 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 51 | expect(content.replace(/\r\n/g, "\n")).to.equal( 52 | "[server.test2]\nurl=http://localhost:8530\ndatabase=_system\nusername=root\npassword=\n" 53 | ); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /lib/test/server-set.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const foxx = require("./util"); 6 | const expect = require("chai").expect; 7 | const os = require("os"); 8 | const fs = require("fs"); 9 | 10 | const foxxRcFile = path.resolve(os.tmpdir(), ".foxxrc"); 11 | 12 | describe("Foxx server set", () => { 13 | beforeEach(async () => { 14 | if (fs.existsSync(foxxRcFile)) { 15 | fs.unlinkSync(foxxRcFile); 16 | } 17 | }); 18 | 19 | it("first executed should create rc file", async () => { 20 | await foxx("server set test //localhost:8529"); 21 | expect(fs.existsSync(foxxRcFile)).to.equal(true); 22 | }); 23 | 24 | it("should add server to rc file", async () => { 25 | await foxx("server set test //localhost:8529"); 26 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 27 | expect(content.replace(/\r\n/g, "\n")).to.equal( 28 | "[server.test]\nurl=http://localhost:8529\ndatabase=_system\nusername=root\npassword=\n" 29 | ); 30 | }); 31 | 32 | it("via alias should add server to rc file", async () => { 33 | await foxx("remote add test //localhost:8529"); 34 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 35 | expect(content.replace(/\r\n/g, "\n")).to.equal( 36 | "[server.test]\nurl=http://localhost:8529\ndatabase=_system\nusername=root\npassword=\n" 37 | ); 38 | }); 39 | 40 | it("should add http server to rc file", async () => { 41 | await foxx("server set test http://localhost:8529"); 42 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 43 | expect(content.replace(/\r\n/g, "\n")).to.equal( 44 | "[server.test]\nurl=http://localhost:8529\ndatabase=_system\nusername=root\npassword=\n" 45 | ); 46 | }); 47 | 48 | it("should add https server to rc file", async () => { 49 | await foxx("server set test https://localhost:8529"); 50 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 51 | expect(content.replace(/\r\n/g, "\n")).to.equal( 52 | "[server.test]\nurl=https://localhost:8529\ndatabase=_system\nusername=root\npassword=\n" 53 | ); 54 | }); 55 | 56 | it("should add tcp server to rc file", async () => { 57 | await foxx("server set test tcp://localhost:8529"); 58 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 59 | expect(content.replace(/\r\n/g, "\n")).to.equal( 60 | "[server.test]\nurl=tcp://localhost:8529\ndatabase=_system\nusername=root\npassword=\n" 61 | ); 62 | }); 63 | 64 | it("should add ssl server to rc file", async () => { 65 | await foxx("server set test ssl://localhost:8529"); 66 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 67 | expect(content.replace(/\r\n/g, "\n")).to.equal( 68 | "[server.test]\nurl=ssl://localhost:8529\ndatabase=_system\nusername=root\npassword=\n" 69 | ); 70 | }); 71 | 72 | it("should add server with credentials to rc file", async () => { 73 | await foxx("server set test http://admin:hunter2@localhost:8529"); 74 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 75 | expect(content.replace(/\r\n/g, "\n")).to.equal( 76 | "[server.test]\nurl=http://admin:hunter2@localhost:8529\ndatabase=_system\n" 77 | ); 78 | }); 79 | 80 | it("should add unix socket server to rc file", async () => { 81 | await foxx("server set test unix:///tmp/arangod.sock"); 82 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 83 | expect(content.replace(/\r\n/g, "\n")).to.equal( 84 | "[server.test]\nurl=unix:///tmp/arangod.sock\ndatabase=_system\nusername=root\npassword=\n" 85 | ); 86 | }); 87 | 88 | it("should add http+unix socket server to rc file", async () => { 89 | await foxx("server set test http+unix:///tmp/arangod.sock"); 90 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 91 | expect(content.replace(/\r\n/g, "\n")).to.equal( 92 | "[server.test]\nurl=http+unix:///tmp/arangod.sock\ndatabase=_system\nusername=root\npassword=\n" 93 | ); 94 | }); 95 | 96 | it("should add http://unix: socket server to rc file", async () => { 97 | await foxx("server set test http://unix:/tmp/arangod.sock"); 98 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 99 | expect(content.replace(/\r\n/g, "\n")).to.equal( 100 | "[server.test]\nurl=http://unix:/tmp/arangod.sock\ndatabase=_system\nusername=root\npassword=\n" 101 | ); 102 | }); 103 | 104 | it("executed two time should add both server to rc file", async () => { 105 | await foxx("server set test1 //localhost:8529"); 106 | await foxx("server set test2 //localhost:8530"); 107 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 108 | expect(content.replace(/\r\n/g, "\n")).to.equal( 109 | "[server.test1]\nurl=http://localhost:8529\ndatabase=_system\nusername=root\npassword=\n\n[server.test2]\nurl=http://localhost:8530\ndatabase=_system\nusername=root\npassword=\n" 110 | ); 111 | }); 112 | 113 | it("should add server with alternative database to rc file", async () => { 114 | await foxx("server set test //localhost:8529 --database test"); 115 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 116 | expect(content.replace(/\r\n/g, "\n")).to.equal( 117 | "[server.test]\nurl=http://localhost:8529\ndatabase=test\nusername=root\npassword=\n" 118 | ); 119 | }); 120 | 121 | it("should add server with alternative database to rc file using alias", async () => { 122 | await foxx("server set test //localhost:8529 -D test"); 123 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 124 | expect(content.replace(/\r\n/g, "\n")).to.equal( 125 | "[server.test]\nurl=http://localhost:8529\ndatabase=test\nusername=root\npassword=\n" 126 | ); 127 | }); 128 | 129 | it("should add server with alternative username to rc file", async () => { 130 | await foxx("server set test //localhost:8529 --username test"); 131 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 132 | expect(content.replace(/\r\n/g, "\n")).to.equal( 133 | "[server.test]\nurl=http://localhost:8529\ndatabase=_system\nusername=test\npassword=\n" 134 | ); 135 | }); 136 | 137 | it("should add server with alternative username to rc file using alias", async () => { 138 | await foxx("server set test //localhost:8529 -u test"); 139 | const content = fs.readFileSync(foxxRcFile, "utf-8"); 140 | expect(content.replace(/\r\n/g, "\n")).to.equal( 141 | "[server.test]\nurl=http://localhost:8529\ndatabase=_system\nusername=test\npassword=\n" 142 | ); 143 | }); 144 | 145 | it("should fail when server URL is not valid", async () => { 146 | try { 147 | await foxx("server set test not-valid"); 148 | } catch (e) { 149 | expect(fs.existsSync(foxxRcFile)).to.equal(false); 150 | return; 151 | } 152 | expect.fail(); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /lib/test/server-show.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const foxx = require("./util"); 6 | const expect = require("chai").expect; 7 | const os = require("os"); 8 | const fs = require("fs"); 9 | 10 | const foxxRcFile = path.resolve(os.tmpdir(), ".foxxrc"); 11 | 12 | describe("Foxx server show", () => { 13 | before(async () => { 14 | if (fs.existsSync(foxxRcFile)) { 15 | fs.unlinkSync(foxxRcFile); 16 | } 17 | await foxx("server set test1 //localhost:8529"); 18 | await foxx("server set test2 //localhost:8530"); 19 | }); 20 | 21 | it("should show added server", async () => { 22 | const server1 = await foxx("server show test1"); 23 | expect(server1).to.equal( 24 | "URL: http://localhost:8529\nDatabase: _system\nUsername: root\nPassword: (hidden)\n" 25 | ); 26 | const server2 = await foxx("server show test2"); 27 | expect(server2).to.equal( 28 | "URL: http://localhost:8530\nDatabase: _system\nUsername: root\nPassword: (hidden)\n" 29 | ); 30 | }); 31 | 32 | it("via alias should show added server", async () => { 33 | const server = await foxx("remote info test1"); 34 | expect(server).to.equal( 35 | "URL: http://localhost:8529\nDatabase: _system\nUsername: root\nPassword: (hidden)\n" 36 | ); 37 | }); 38 | 39 | it("verbose should show added server with password", async () => { 40 | const server = await foxx("server show test1 --verbose"); 41 | expect(server).to.equal( 42 | "URL: http://localhost:8529\nDatabase: _system\nUsername: root\nPassword: (empty)\n" 43 | ); 44 | }); 45 | 46 | it("verbose via alias should show added server with password", async () => { 47 | const server = await foxx("server show test1 -v"); 48 | expect(server).to.equal( 49 | "URL: http://localhost:8529\nDatabase: _system\nUsername: root\nPassword: (empty)\n" 50 | ); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /lib/test/server.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, beforeEach, after, afterEach */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const { Database } = require("arangojs"); 6 | const foxx = require("./util"); 7 | const expect = require("chai").expect; 8 | const os = require("os"); 9 | const fs = require("fs"); 10 | 11 | const ARANGO_VERSION = Number(process.env.ARANGO_VERSION || 30000); 12 | const ARANGO_URL = process.env.TEST_ARANGODB_URL || "http://localhost:8529"; 13 | 14 | const mount = "/server-test"; 15 | const basePath = path.resolve(__dirname, "..", "..", "fixtures"); 16 | 17 | const foxxRcFile = path.resolve(os.tmpdir(), ".foxxrc"); 18 | 19 | describe("Foxx with server", () => { 20 | const db = new Database({ 21 | url: ARANGO_URL, 22 | arangoVersion: ARANGO_VERSION, 23 | }); 24 | 25 | before(async () => { 26 | process.env.FOXXRC_PATH = foxxRcFile; 27 | }); 28 | 29 | after(async () => { 30 | process.env.FOXXRC_PATH = undefined; 31 | }); 32 | 33 | beforeEach(async () => { 34 | if (fs.existsSync(foxxRcFile)) { 35 | fs.unlinkSync(foxxRcFile); 36 | } 37 | }); 38 | 39 | afterEach(async () => { 40 | try { 41 | await await foxx(`uninstall ${mount}`); 42 | } catch (e) { 43 | // noop 44 | } 45 | }); 46 | 47 | it("with alternative server should be available", async () => { 48 | await foxx(`server set test ${ARANGO_URL}`); 49 | await foxx( 50 | `install --server test ${mount} ${path.resolve( 51 | basePath, 52 | "minimal-working-service.zip" 53 | )}` 54 | ); 55 | const res = await db.route(mount).get(); 56 | expect(res.body).to.eql({ hello: "world" }); 57 | }); 58 | }); 59 | 60 | describe("Foxx with non-default db", () => { 61 | const dbName = `test_cli_${Date.now()}`; 62 | let db; 63 | const system = new Database({ 64 | url: ARANGO_URL, 65 | arangoVersion: ARANGO_VERSION, 66 | }); 67 | 68 | before(async () => { 69 | db = await system.createDatabase(dbName); 70 | process.env.FOXXRC_PATH = foxxRcFile; 71 | }); 72 | 73 | after(async () => { 74 | await system.dropDatabase(db.name); 75 | process.env.FOXXRC_PATH = undefined; 76 | }); 77 | 78 | beforeEach(async () => { 79 | if (fs.existsSync(foxxRcFile)) { 80 | fs.unlinkSync(foxxRcFile); 81 | } 82 | }); 83 | 84 | afterEach(async () => { 85 | try { 86 | await await foxx(`uninstall ${mount}`); 87 | } catch (e) { 88 | // noop 89 | } 90 | }); 91 | 92 | it("with alternative server should be available", async () => { 93 | await foxx(`server set test ${ARANGO_URL} -D ${dbName}`); 94 | await foxx( 95 | `install --server test ${mount} ${path.resolve( 96 | basePath, 97 | "minimal-working-service.zip" 98 | )}` 99 | ); 100 | const res = await db.route(mount).get(); 101 | expect(res.body).to.eql({ hello: "world" }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /lib/test/set-dev.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, after, beforeEach, afterEach */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const { Database } = require("arangojs"); 6 | const expect = require("chai").expect; 7 | const foxx = require("./util"); 8 | const fs = require("fs"); 9 | 10 | const ARANGO_VERSION = Number(process.env.ARANGO_VERSION || 30000); 11 | const ARANGO_URL = process.env.TEST_ARANGODB_URL || "http://localhost:8529"; 12 | const ARANGO_USERNAME = process.env.ARANGO_USERNAME || "root"; 13 | 14 | const mount = "/dev-test"; 15 | const basePath = path.resolve(__dirname, "..", "..", "fixtures"); 16 | 17 | describe("Foxx service development mode", () => { 18 | const db = new Database({ 19 | url: ARANGO_URL, 20 | arangoVersion: ARANGO_VERSION, 21 | }); 22 | 23 | beforeEach(async () => { 24 | await db.installService( 25 | mount, 26 | fs.readFileSync(path.resolve(basePath, "minimal-working-service.zip")) 27 | ); 28 | }); 29 | 30 | afterEach(async () => { 31 | try { 32 | await db.uninstallService(mount, { force: true }); 33 | } catch (e) { 34 | // noop 35 | } 36 | }); 37 | 38 | it("should be activated", async () => { 39 | const infoBefore = await db.getService(mount); 40 | expect(infoBefore.development).to.equal(false); 41 | await foxx(`set-dev ${mount}`); 42 | const infoAfter = await db.getService(mount); 43 | expect(infoAfter.development).to.equal(true); 44 | }); 45 | 46 | it("should be activated via alias", async () => { 47 | const infoBefore = await db.getService(mount); 48 | expect(infoBefore.development).to.equal(false); 49 | await foxx(`set-development ${mount}`); 50 | const infoAfter = await db.getService(mount); 51 | expect(infoAfter.development).to.equal(true); 52 | }); 53 | 54 | it("with alternative server URL should be activated", async () => { 55 | const infoBefore = await db.getService(mount); 56 | expect(infoBefore.development).to.equal(false); 57 | await foxx(`set-dev --server ${ARANGO_URL} ${mount}`); 58 | const infoAfter = await db.getService(mount); 59 | expect(infoAfter.development).to.equal(true); 60 | }); 61 | 62 | it("with alternative server URL (short option) should be activated", async () => { 63 | const infoBefore = await db.getService(mount); 64 | expect(infoBefore.development).to.equal(false); 65 | await foxx(`set-dev -H ${ARANGO_URL} ${mount}`); 66 | const infoAfter = await db.getService(mount); 67 | expect(infoAfter.development).to.equal(true); 68 | }); 69 | 70 | it("with alternative database should be activated", async () => { 71 | const infoBefore = await db.getService(mount); 72 | expect(infoBefore.development).to.equal(false); 73 | await foxx(`set-dev --database _system ${mount}`); 74 | const infoAfter = await db.getService(mount); 75 | expect(infoAfter.development).to.equal(true); 76 | }); 77 | 78 | it("with alternative database (short option) should be activated", async () => { 79 | const infoBefore = await db.getService(mount); 80 | expect(infoBefore.development).to.equal(false); 81 | await foxx(`set-dev -D _system ${mount}`); 82 | const infoAfter = await db.getService(mount); 83 | expect(infoAfter.development).to.equal(true); 84 | }); 85 | 86 | it("with alternative username should be activated", async () => { 87 | const infoBefore = await db.getService(mount); 88 | expect(infoBefore.development).to.equal(false); 89 | await foxx(`set-dev --username ${ARANGO_USERNAME} ${mount}`); 90 | const infoAfter = await db.getService(mount); 91 | expect(infoAfter.development).to.equal(true); 92 | }); 93 | 94 | it("with alternative username should be activated (short option)", async () => { 95 | const infoBefore = await db.getService(mount); 96 | expect(infoBefore.development).to.equal(false); 97 | await foxx(`set-dev -u ${ARANGO_USERNAME} ${mount}`); 98 | const infoAfter = await db.getService(mount); 99 | expect(infoAfter.development).to.equal(true); 100 | }); 101 | 102 | describe("with a password file", () => { 103 | const user = "testuser"; 104 | const passwordFilePath = path.resolve(basePath, "passwordFile"); 105 | const passwd = fs.readFileSync(passwordFilePath, "utf-8"); 106 | before(async () => { 107 | db.route("/_api/user").post({ 108 | user, 109 | passwd, 110 | }); 111 | db.route(`/_api/user/${user}/database/_system`).put({ grant: "rw" }); 112 | }); 113 | after(async () => { 114 | try { 115 | db.route(`/_api/user/${user}`).delete(); 116 | } catch (e) { 117 | // noop 118 | } 119 | }); 120 | it("should be activated", async () => { 121 | const infoBefore = await db.getService(mount); 122 | expect(infoBefore.development).to.equal(false); 123 | await foxx( 124 | `set-dev --username ${user} --password-file ${passwordFilePath} ${mount}` 125 | ); 126 | const infoAfter = await db.getService(mount); 127 | expect(infoAfter.development).to.equal(true); 128 | }); 129 | }); 130 | 131 | it("should fail when mount is invalid", async () => { 132 | try { 133 | await foxx("set-dev /dev/null"); 134 | } catch (e) { 135 | return; 136 | } 137 | expect.fail(); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /lib/test/set-prod.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, after, beforeEach, afterEach */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const { Database } = require("arangojs"); 6 | const expect = require("chai").expect; 7 | const foxx = require("./util"); 8 | const fs = require("fs"); 9 | 10 | const ARANGO_VERSION = Number(process.env.ARANGO_VERSION || 30000); 11 | const ARANGO_URL = process.env.TEST_ARANGODB_URL || "http://localhost:8529"; 12 | const ARANGO_USERNAME = process.env.ARANGO_USERNAME || "root"; 13 | 14 | const mount = "/prod-test"; 15 | const basePath = path.resolve(__dirname, "..", "..", "fixtures"); 16 | 17 | describe("Foxx service production mode", () => { 18 | const db = new Database({ 19 | url: ARANGO_URL, 20 | arangoVersion: ARANGO_VERSION, 21 | }); 22 | 23 | beforeEach(async () => { 24 | await db.installService( 25 | mount, 26 | fs.readFileSync(path.resolve(basePath, "minimal-working-service.zip")) 27 | ); 28 | await db.setServiceDevelopmentMode(mount, true); 29 | }); 30 | 31 | afterEach(async () => { 32 | try { 33 | await db.uninstallService(mount, { force: true }); 34 | } catch (e) { 35 | // noop 36 | } 37 | }); 38 | 39 | it("should be activated", async () => { 40 | const infoBefore = await db.getService(mount); 41 | expect(infoBefore.development).to.equal(true); 42 | await foxx(`set-prod ${mount}`); 43 | const infoAfter = await db.getService(mount); 44 | expect(infoAfter.development).to.equal(false); 45 | }); 46 | 47 | it("should be activated via alias", async () => { 48 | const infoBefore = await db.getService(mount); 49 | expect(infoBefore.development).to.equal(true); 50 | await foxx(`set-production ${mount}`); 51 | const infoAfter = await db.getService(mount); 52 | expect(infoAfter.development).to.equal(false); 53 | }); 54 | 55 | it("with alternative server URL should be activated", async () => { 56 | const infoBefore = await db.getService(mount); 57 | expect(infoBefore.development).to.equal(true); 58 | await foxx(`set-prod --server ${ARANGO_URL} ${mount}`); 59 | const infoAfter = await db.getService(mount); 60 | expect(infoAfter.development).to.equal(false); 61 | }); 62 | 63 | it("with alternative server URL (short option) should be activated", async () => { 64 | const infoBefore = await db.getService(mount); 65 | expect(infoBefore.development).to.equal(true); 66 | await foxx(`set-prod -H ${ARANGO_URL} ${mount}`); 67 | const infoAfter = await db.getService(mount); 68 | expect(infoAfter.development).to.equal(false); 69 | }); 70 | 71 | it("with alternative database should be activated", async () => { 72 | const infoBefore = await db.getService(mount); 73 | expect(infoBefore.development).to.equal(true); 74 | await foxx(`set-prod --database _system ${mount}`); 75 | const infoAfter = await db.getService(mount); 76 | expect(infoAfter.development).to.equal(false); 77 | }); 78 | 79 | it("with alternative database (short option) should be activated", async () => { 80 | const infoBefore = await db.getService(mount); 81 | expect(infoBefore.development).to.equal(true); 82 | await foxx(`set-prod -D _system ${mount}`); 83 | const infoAfter = await db.getService(mount); 84 | expect(infoAfter.development).to.equal(false); 85 | }); 86 | 87 | it("with alternative username should be activated", async () => { 88 | const infoBefore = await db.getService(mount); 89 | expect(infoBefore.development).to.equal(true); 90 | await foxx(`set-prod --username ${ARANGO_USERNAME} ${mount}`); 91 | const infoAfter = await db.getService(mount); 92 | expect(infoAfter.development).to.equal(false); 93 | }); 94 | 95 | it("with alternative username should be activated (short option)", async () => { 96 | const infoBefore = await db.getService(mount); 97 | expect(infoBefore.development).to.equal(true); 98 | await foxx(`set-prod -u ${ARANGO_USERNAME} ${mount}`); 99 | const infoAfter = await db.getService(mount); 100 | expect(infoAfter.development).to.equal(false); 101 | }); 102 | 103 | describe("with a password file", () => { 104 | const user = "testuser"; 105 | const passwordFilePath = path.resolve(basePath, "passwordFile"); 106 | const passwd = fs.readFileSync(passwordFilePath, "utf-8"); 107 | before(async () => { 108 | db.route("/_api/user").post({ 109 | user, 110 | passwd, 111 | }); 112 | db.route(`/_api/user/${user}/database/_system`).put({ grant: "rw" }); 113 | }); 114 | after(async () => { 115 | try { 116 | db.route(`/_api/user/${user}`).delete(); 117 | } catch (e) { 118 | // noop 119 | } 120 | }); 121 | it("should be activated", async () => { 122 | const infoBefore = await db.getService(mount); 123 | expect(infoBefore.development).to.equal(true); 124 | await foxx( 125 | `set-prod --username ${user} --password-file ${passwordFilePath} ${mount}` 126 | ); 127 | const infoAfter = await db.getService(mount); 128 | expect(infoAfter.development).to.equal(false); 129 | }); 130 | }); 131 | 132 | it("should fail when mount is invalid", async () => { 133 | try { 134 | await foxx("set-prod /dev/null"); 135 | } catch (e) { 136 | return; 137 | } 138 | expect.fail(); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /lib/test/show.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, after */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const { Database } = require("arangojs"); 6 | const foxx = require("./util"); 7 | const expect = require("chai").expect; 8 | const fs = require("fs"); 9 | 10 | const ARANGO_VERSION = Number(process.env.ARANGO_VERSION || 30000); 11 | const ARANGO_URL = process.env.TEST_ARANGODB_URL || "http://localhost:8529"; 12 | const ARANGO_USERNAME = process.env.ARANGO_USERNAME || "root"; 13 | 14 | const mount = "/show-test"; 15 | const basePath = path.resolve(__dirname, "..", "..", "fixtures"); 16 | 17 | describe("Foxx service show", () => { 18 | const db = new Database({ 19 | url: ARANGO_URL, 20 | arangoVersion: ARANGO_VERSION, 21 | }); 22 | 23 | before(async () => { 24 | await db.installService( 25 | mount, 26 | fs.readFileSync(path.resolve(basePath, "minimal-working-service.zip")) 27 | ); 28 | }); 29 | 30 | after(async () => { 31 | try { 32 | await db.uninstallService(mount, { force: true }); 33 | } catch (e) { 34 | // noop 35 | } 36 | }); 37 | 38 | it("should show information about the service", async () => { 39 | const service = await foxx(`show ${mount}`, true); 40 | expect(service).to.have.property("name", "minimal-working-manifest"); 41 | expect(service).to.have.property("version", "0.0.0"); 42 | expect(service).to.have.property("development", false); 43 | expect(service).to.have.property("legacy", false); 44 | }); 45 | 46 | it("should show information about the service via alias", async () => { 47 | const service = await foxx(`info ${mount}`, true); 48 | expect(service).to.have.property("name", "minimal-working-manifest"); 49 | expect(service).to.have.property("version", "0.0.0"); 50 | expect(service).to.have.property("development", false); 51 | expect(service).to.have.property("legacy", false); 52 | }); 53 | 54 | it("with alternative server URL should show information about the service", async () => { 55 | const service = await foxx(`show --server ${ARANGO_URL} ${mount}`, true); 56 | expect(service).to.have.property("name", "minimal-working-manifest"); 57 | expect(service).to.have.property("version", "0.0.0"); 58 | expect(service).to.have.property("development", false); 59 | expect(service).to.have.property("legacy", false); 60 | }); 61 | 62 | it("with alternative server URL (short option) should show information about the service", async () => { 63 | const service = await foxx(`show -H ${ARANGO_URL} ${mount}`, true); 64 | expect(service).to.have.property("name", "minimal-working-manifest"); 65 | expect(service).to.have.property("version", "0.0.0"); 66 | expect(service).to.have.property("development", false); 67 | expect(service).to.have.property("legacy", false); 68 | }); 69 | 70 | it("with alternative database should show information about the service", async () => { 71 | const service = await foxx(`show --database _system ${mount}`, true); 72 | expect(service).to.have.property("name", "minimal-working-manifest"); 73 | expect(service).to.have.property("version", "0.0.0"); 74 | expect(service).to.have.property("development", false); 75 | expect(service).to.have.property("legacy", false); 76 | }); 77 | 78 | it("with alternative database (short option) should show information about the service", async () => { 79 | const service = await foxx(`show -D _system ${mount}`, true); 80 | expect(service).to.have.property("name", "minimal-working-manifest"); 81 | expect(service).to.have.property("version", "0.0.0"); 82 | expect(service).to.have.property("development", false); 83 | expect(service).to.have.property("legacy", false); 84 | }); 85 | 86 | it("with alternative username should show information about the service", async () => { 87 | const service = await foxx( 88 | `show --username ${ARANGO_USERNAME} ${mount}`, 89 | true 90 | ); 91 | expect(service).to.have.property("name", "minimal-working-manifest"); 92 | expect(service).to.have.property("version", "0.0.0"); 93 | expect(service).to.have.property("development", false); 94 | expect(service).to.have.property("legacy", false); 95 | }); 96 | 97 | it("with alternative username should show information about the service (short option)", async () => { 98 | const service = await foxx(`show -u ${ARANGO_USERNAME} ${mount}`, true); 99 | expect(service).to.have.property("name", "minimal-working-manifest"); 100 | expect(service).to.have.property("version", "0.0.0"); 101 | expect(service).to.have.property("development", false); 102 | expect(service).to.have.property("legacy", false); 103 | }); 104 | 105 | describe("with a password file", () => { 106 | const user = "testuser"; 107 | const passwordFilePath = path.resolve(basePath, "passwordFile"); 108 | const passwd = fs.readFileSync(passwordFilePath, "utf-8"); 109 | before(async () => { 110 | db.route("/_api/user").post({ 111 | user, 112 | passwd, 113 | }); 114 | db.route(`/_api/user/${user}/database/_system`).put({ grant: "rw" }); 115 | }); 116 | after(async () => { 117 | try { 118 | db.route(`/_api/user/${user}`).delete(); 119 | } catch (e) { 120 | // noop 121 | } 122 | }); 123 | it("should show information about the service", async () => { 124 | const service = await foxx( 125 | `show --username ${user} --password-file ${passwordFilePath} ${mount}`, 126 | true 127 | ); 128 | expect(service).to.have.property("name", "minimal-working-manifest"); 129 | expect(service).to.have.property("version", "0.0.0"); 130 | expect(service).to.have.property("development", false); 131 | expect(service).to.have.property("legacy", false); 132 | }); 133 | }); 134 | 135 | it("should fail when mount is invalid", async () => { 136 | try { 137 | await foxx("show /dev/null"); 138 | } catch (e) { 139 | return; 140 | } 141 | expect.fail(); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /lib/test/test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, after */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const { Database } = require("arangojs"); 6 | const foxx = require("./util"); 7 | const expect = require("chai").expect; 8 | const fs = require("fs"); 9 | 10 | const ARANGO_VERSION = Number(process.env.ARANGO_VERSION || 30000); 11 | const ARANGO_URL = process.env.TEST_ARANGODB_URL || "http://localhost:8529"; 12 | const ARANGO_USERNAME = process.env.ARANGO_USERNAME || "root"; 13 | 14 | const mount = "/test-test"; 15 | const basePath = path.resolve(__dirname, "..", "..", "fixtures"); 16 | 17 | describe("Foxx service test", () => { 18 | const db = new Database({ 19 | url: ARANGO_URL, 20 | arangoVersion: ARANGO_VERSION, 21 | }); 22 | 23 | before(async () => { 24 | await db.installService( 25 | mount, 26 | fs.readFileSync(path.resolve(basePath, "with-tests.zip")) 27 | ); 28 | }); 29 | 30 | after(async () => { 31 | try { 32 | await db.uninstallService(mount, { force: true }); 33 | } catch (e) { 34 | // noop 35 | } 36 | }); 37 | 38 | it("should print test result", async () => { 39 | try { 40 | await foxx(`test ${mount}`); 41 | } catch (e) { 42 | const result = e.stdout.toString("utf-8"); 43 | expect(result).to.has.string("4 passing"); 44 | expect(result).to.has.string("2 failing"); 45 | } 46 | }); 47 | 48 | it("with alternative server URL should print test result", async () => { 49 | try { 50 | await foxx(`test ${mount} --server ${ARANGO_URL}`); 51 | } catch (e) { 52 | const result = e.stdout.toString("utf-8"); 53 | expect(result).to.has.string("4 passing"); 54 | expect(result).to.has.string("2 failing"); 55 | } 56 | }); 57 | 58 | it("with alternative server URL (short option) should print test result", async () => { 59 | try { 60 | await foxx(`test ${mount} -H ${ARANGO_URL}`); 61 | } catch (e) { 62 | const result = e.stdout.toString("utf-8"); 63 | expect(result).to.has.string("4 passing"); 64 | expect(result).to.has.string("2 failing"); 65 | } 66 | }); 67 | 68 | it("with alternative database should print test result", async () => { 69 | try { 70 | await foxx(`test ${mount} --database _system`); 71 | } catch (e) { 72 | const result = e.stdout.toString("utf-8"); 73 | expect(result).to.has.string("4 passing"); 74 | expect(result).to.has.string("2 failing"); 75 | } 76 | }); 77 | 78 | it("with alternative database (short option) should print test result", async () => { 79 | try { 80 | await foxx(`test ${mount} -D _system`); 81 | } catch (e) { 82 | const result = e.stdout.toString("utf-8"); 83 | expect(result).to.has.string("4 passing"); 84 | expect(result).to.has.string("2 failing"); 85 | } 86 | }); 87 | 88 | it("with alternative username should print test result", async () => { 89 | try { 90 | await foxx(`test ${mount} --username ${ARANGO_USERNAME}`); 91 | } catch (e) { 92 | const result = e.stdout.toString("utf-8"); 93 | expect(result).to.has.string("4 passing"); 94 | expect(result).to.has.string("2 failing"); 95 | } 96 | }); 97 | 98 | it("with alternative username should print test result (short option)", async () => { 99 | try { 100 | await foxx(`test ${mount} -u ${ARANGO_USERNAME}`); 101 | } catch (e) { 102 | const result = e.stdout.toString("utf-8"); 103 | expect(result).to.has.string("4 passing"); 104 | expect(result).to.has.string("2 failing"); 105 | } 106 | }); 107 | 108 | describe("with a password file", () => { 109 | const user = "testuser"; 110 | const passwordFilePath = path.resolve(basePath, "passwordFile"); 111 | const passwd = fs.readFileSync(passwordFilePath, "utf-8"); 112 | before(async () => { 113 | db.route("/_api/user").post({ 114 | user, 115 | passwd, 116 | }); 117 | db.route(`/_api/user/${user}/database/_system`).put({ grant: "rw" }); 118 | }); 119 | after(async () => { 120 | try { 121 | db.route(`/_api/user/${user}`).delete(); 122 | } catch (e) { 123 | // noop 124 | } 125 | }); 126 | it("should print test result", async () => { 127 | try { 128 | await foxx( 129 | `test ${mount} --username ${user} --password-file ${passwordFilePath}` 130 | ); 131 | } catch (e) { 132 | const result = e.stdout.toString("utf-8"); 133 | expect(result).to.has.string("4 passing"); 134 | expect(result).to.has.string("2 failing"); 135 | } 136 | }); 137 | }); 138 | 139 | it("should fail when mount is invalid", async () => { 140 | try { 141 | await foxx(`test /dev/null echo`); 142 | } catch (e) { 143 | return; 144 | } 145 | expect.fail(); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /lib/test/uninstall.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, after, beforeEach, afterEach */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const { Database } = require("arangojs"); 6 | const expect = require("chai").expect; 7 | const foxx = require("./util"); 8 | const fs = require("fs"); 9 | 10 | const ARANGO_VERSION = Number(process.env.ARANGO_VERSION || 30000); 11 | const ARANGO_URL = process.env.TEST_ARANGODB_URL || "http://localhost:8529"; 12 | const ARANGO_USERNAME = process.env.ARANGO_USERNAME || "root"; 13 | 14 | const mount = "/uninstall-test"; 15 | const basePath = path.resolve(__dirname, "..", "..", "fixtures"); 16 | 17 | describe("Foxx service uninstalled", () => { 18 | const db = new Database({ 19 | url: ARANGO_URL, 20 | arangoVersion: ARANGO_VERSION, 21 | }); 22 | 23 | beforeEach(async () => { 24 | try { 25 | await db.installService( 26 | mount, 27 | fs.readFileSync(path.resolve(basePath, "minimal-working-service.zip")) 28 | ); 29 | } catch (e) { 30 | // noop 31 | } 32 | }); 33 | 34 | afterEach(async () => { 35 | try { 36 | await db.uninstallService(mount, { force: true }); 37 | } catch (e) { 38 | // noop 39 | } 40 | }); 41 | 42 | it("via alias remove should not be available", async () => { 43 | await foxx(`remove ${mount}`); 44 | try { 45 | await db.route(mount).get(); 46 | expect.fail(); 47 | } catch (e) { 48 | expect(e).to.have.property("code", 404); 49 | } 50 | }); 51 | 52 | it("via alias purge should not be available", async () => { 53 | await foxx(`purge ${mount}`); 54 | try { 55 | await db.route(mount).get(); 56 | expect.fail(); 57 | } catch (e) { 58 | expect(e).to.have.property("code", 404); 59 | } 60 | }); 61 | 62 | it("with alternative server URL should not be available", async () => { 63 | await foxx(`uninstall --server ${ARANGO_URL} ${mount}`); 64 | try { 65 | await db.route(mount).get(); 66 | expect.fail(); 67 | } catch (e) { 68 | expect(e).to.have.property("code", 404); 69 | } 70 | }); 71 | 72 | it("with alternative server URL (short option) should not be available", async () => { 73 | await foxx(`uninstall -H ${ARANGO_URL} ${mount}`); 74 | try { 75 | await db.route(mount).get(); 76 | expect.fail(); 77 | } catch (e) { 78 | expect(e).to.have.property("code", 404); 79 | } 80 | }); 81 | 82 | it("with alternative database should not be available", async () => { 83 | await foxx(`uninstall --database _system ${mount}`); 84 | try { 85 | await db.route(mount).get(); 86 | expect.fail(); 87 | } catch (e) { 88 | expect(e).to.have.property("code", 404); 89 | } 90 | }); 91 | 92 | it("with alternative database (short option) should not be available", async () => { 93 | await foxx(`uninstall -D _system ${mount}`); 94 | try { 95 | await db.route(mount).get(); 96 | expect.fail(); 97 | } catch (e) { 98 | expect(e).to.have.property("code", 404); 99 | } 100 | }); 101 | 102 | it("with alternative username should be available", async () => { 103 | await foxx(`uninstall --username ${ARANGO_USERNAME} ${mount}`); 104 | try { 105 | await db.route(mount).get(); 106 | expect.fail(); 107 | } catch (e) { 108 | expect(e).to.have.property("code", 404); 109 | } 110 | }); 111 | 112 | it("with alternative username should be available (short option)", async () => { 113 | await foxx(`uninstall -u ${ARANGO_USERNAME} ${mount}`); 114 | try { 115 | await db.route(mount).get(); 116 | expect.fail(); 117 | } catch (e) { 118 | expect(e).to.have.property("code", 404); 119 | } 120 | }); 121 | 122 | describe("with a password file", () => { 123 | const user = "testuser"; 124 | const passwordFilePath = path.resolve(basePath, "passwordFile"); 125 | const passwd = fs.readFileSync(passwordFilePath, "utf-8"); 126 | before(async () => { 127 | db.route("/_api/user").post({ 128 | user, 129 | passwd, 130 | }); 131 | db.route(`/_api/user/${user}/database/_system`).put({ grant: "rw" }); 132 | }); 133 | after(async () => { 134 | try { 135 | db.route(`/_api/user/${user}`).delete(); 136 | } catch (e) { 137 | // noop 138 | } 139 | }); 140 | it("should not be available", async () => { 141 | await foxx( 142 | `uninstall --username ${user} --password-file ${passwordFilePath} ${mount}` 143 | ); 144 | try { 145 | await db.route(mount).get(); 146 | expect.fail(); 147 | } catch (e) { 148 | expect(e).to.have.property("code", 404); 149 | } 150 | }); 151 | }); 152 | 153 | it("should run its teardown script by default", async () => { 154 | const col = `${mount}_setup_teardown`.replace(/\//, "").replace(/-/g, "_"); 155 | await foxx( 156 | `replace ${mount} ${path.resolve( 157 | basePath, 158 | "minimal-working-setup-teardown.zip" 159 | )}` 160 | ); 161 | const info = await db.collection(col).get(); 162 | expect(info).to.have.property("name", col); 163 | await foxx(`uninstall ${mount}`); 164 | try { 165 | await db.collection(col).get(); 166 | expect.fail(); 167 | } catch (e) { 168 | expect(e.errorNum).to.equal(1203); 169 | } 170 | }); 171 | 172 | it("should run its teardown script when enabled", async () => { 173 | const col = `${mount}_setup_teardown`.replace(/\//, "").replace(/-/g, "_"); 174 | await foxx( 175 | `replace ${mount} ${path.resolve( 176 | basePath, 177 | "minimal-working-setup-teardown.zip" 178 | )}` 179 | ); 180 | await foxx(`uninstall --teardown ${mount}`); 181 | try { 182 | await db.collection(col).get(); 183 | expect.fail(); 184 | } catch (e) { 185 | expect(e.errorNum).to.equal(1203); 186 | } 187 | }); 188 | 189 | it("should not run its teardown script when disabled", async () => { 190 | const col = `${mount}_setup_teardown`.replace(/\//, "").replace(/-/g, "_"); 191 | try { 192 | await foxx( 193 | `replace ${mount} ${path.resolve( 194 | basePath, 195 | "minimal-working-setup-teardown.zip" 196 | )}` 197 | ); 198 | await foxx(`uninstall --no-teardown ${mount}`); 199 | const info = await db.collection(col).get(); 200 | expect(info).to.have.property("name", col); 201 | } finally { 202 | try { 203 | await db.collection(col).drop(); 204 | } catch (e) { 205 | // noop 206 | } 207 | } 208 | }); 209 | 210 | it("should not fail when mount is invalid", async () => { 211 | await foxx("uninstall /dev/null"); 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /lib/test/util/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const execFile = require("child_process").execFile; 4 | const os = require("os"); 5 | const path = require("path"); 6 | 7 | const foxxRcFile = path.resolve(os.tmpdir(), ".foxxrc"); 8 | const ARANGO_URL = process.env.TEST_ARANGODB_URL || "http://localhost:8529"; 9 | const SERVER_COMMANDS = [ 10 | "config", 11 | "cfg", 12 | "configuration", 13 | "deps", 14 | "dep", 15 | "dependencies", 16 | "download", 17 | "dl", 18 | "install", 19 | "i", 20 | "list", 21 | "replace", 22 | "run", 23 | "script", 24 | "scripts", 25 | "set-dev", 26 | "set-development", 27 | "set-prod", 28 | "set-production", 29 | "show", 30 | "info", 31 | "test", 32 | "uninstall", 33 | "remove", 34 | "purge", 35 | "upgrade", 36 | ]; 37 | 38 | module.exports = (command, raw = false, { input, ...options } = {}) => 39 | new Promise((resolve, reject) => { 40 | const foxx = path.resolve(__dirname, "..", "..", "..", "bin", "foxx"); 41 | try { 42 | const parts = command.split(" "); 43 | if ( 44 | SERVER_COMMANDS.includes(parts[0]) && 45 | !parts.includes("--server") && 46 | !parts.includes("-H") 47 | ) { 48 | parts.splice(1, 0, "--server", ARANGO_URL); 49 | } 50 | const proc = execFile( 51 | "node", 52 | raw ? [foxx, ...parts, "--raw"] : [foxx, ...parts], 53 | { 54 | ...options, 55 | env: { ...options.env, FORCE_COLOR: "0", FOXXRC_PATH: foxxRcFile }, 56 | }, 57 | (err, stdout, stderr) => { 58 | if (err) { 59 | err.stdout = stdout; 60 | err.stderr = stderr; 61 | reject(err); 62 | } else if (raw) { 63 | resolve(JSON.parse(stdout)); 64 | } else { 65 | resolve(stdout.toString("utf-8")); 66 | } 67 | } 68 | ); 69 | if (input) { 70 | proc.stdin.write(input); 71 | proc.stdin.end(); 72 | } 73 | } catch (e) { 74 | reject(e); 75 | } 76 | }); 77 | -------------------------------------------------------------------------------- /lib/util/array.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.uniq = function uniq(arr) { 3 | return Array.from(new Set(arr)); 4 | }; 5 | 6 | exports.splat = function splat(arr) { 7 | if (Array.isArray(arr)) return arr; 8 | return [arr]; 9 | }; 10 | 11 | exports.unsplat = function unsplat(arr) { 12 | if (!Array.isArray(arr)) return arr; 13 | return arr[arr.length - 1]; 14 | }; 15 | -------------------------------------------------------------------------------- /lib/util/cli.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { white, bold } = require("chalk"); 3 | const { splat, unsplat } = require("./array"); 4 | const { fatal } = require("./log"); 5 | const { group, inline: il } = require("./text"); 6 | 7 | exports.common = function common(yargs, opts) { 8 | yargs = yargs 9 | .epilog("Copyright (c) 2016-2017 ArangoDB GmbH (https://foxx.arangodb.com)") 10 | .strict(); 11 | 12 | if (opts) { 13 | let usage = "Usage: $0"; 14 | if (opts.sub) usage += " " + opts.sub; 15 | if (opts.command) usage += " " + opts.command; 16 | if (opts.aliases) usage += "\nAliases: " + opts.aliases.join(", "); 17 | if (opts.describe) usage += "\n\n" + opts.describe; 18 | if (opts.args) usage += "\n\n" + group("Arguments", ...opts.args); 19 | yargs.usage(usage); 20 | } 21 | 22 | return yargs; 23 | }; 24 | 25 | exports.serverArgs = { 26 | server: { 27 | describe: "ArangoDB server URL or alias", 28 | alias: "H", 29 | type: "string", 30 | default: "default", 31 | }, 32 | username: { 33 | describe: "Username to authenticate with", 34 | alias: "u", 35 | type: "string", 36 | }, 37 | password: { 38 | describe: "Use password to authenticate", 39 | alias: "P", 40 | type: "boolean", 41 | default: false, 42 | }, 43 | "password-file": { 44 | describe: "Read a password from a file to authenticate", 45 | alias: "p", 46 | type: "string", 47 | }, 48 | token: { 49 | describe: "Use bearer token to authenticate", 50 | alias: "T", 51 | type: "boolean", 52 | default: false, 53 | }, 54 | database: { 55 | describe: "ArangoDB database name", 56 | alias: "D", 57 | type: "string", 58 | }, 59 | }; 60 | 61 | exports.parseServiceOptions = function parseServiceOptions(argv) { 62 | if (argv.source) argv.source = unsplat(argv.source); 63 | if (argv.cfg) argv.cfg = splat(argv.cfg); 64 | if (argv.dep) argv.dep = splat(argv.dep); 65 | 66 | if (argv.remote) { 67 | if (!argv.source || argv.source === "@") { 68 | fatal( 69 | `Please specify a URL or file path when using ${bold("--remote")}.` 70 | ); 71 | } 72 | } else if (!argv.source) { 73 | argv.source = process.cwd(); 74 | } 75 | 76 | const configuration = {}; 77 | if (argv.cfg) { 78 | for (const cfg of argv.cfg) { 79 | const i = cfg.indexOf("="); 80 | if (i === -1 || i === 0) { 81 | fatal(il` 82 | Configuration options must be specified as name=value pairs. 83 | Option "${white(cfg)}" is invalid. 84 | `); 85 | } 86 | 87 | const name = cfg.slice(0, i); 88 | const value = cfg.slice(i + 1); 89 | 90 | try { 91 | configuration[name] = value ? JSON.parse(value) : null; 92 | } catch (e) { 93 | fatal(il` 94 | Configuration option "${white( 95 | name 96 | )}" is invalid. Value must be valid JSON: "${white(value)}". 97 | `); 98 | } 99 | } 100 | } 101 | 102 | const dependencies = {}; 103 | if (argv.dep) { 104 | for (const dep of argv.dep) { 105 | const i = dep.indexOf("="); 106 | if (i === -1 || i === 0) { 107 | fatal(il` 108 | Dependency options must be specified as name=/path pairs. 109 | Option "${white(dep)}" is invalid. 110 | `); 111 | } 112 | 113 | const name = dep.slice(0, i); 114 | const value = dep.slice(i + 1); 115 | 116 | if (dependencies[name]) { 117 | if (!Array.isArray(dependencies[name])) { 118 | dependencies[name] = [dependencies[name]]; 119 | } 120 | dependencies[name].push(value); 121 | } else { 122 | dependencies[name] = value; 123 | } 124 | } 125 | } 126 | const opts = { configuration, dependencies }; 127 | if (argv.development) opts.development = true; 128 | if (argv.legacy) opts.legacy = true; 129 | return opts; 130 | }; 131 | -------------------------------------------------------------------------------- /lib/util/client.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { Database } = require("arangojs"); 3 | 4 | module.exports = function (server) { 5 | const db = new Database({ url: server.url, databaseName: server.database }); 6 | if (server.token) { 7 | db.useBearerAuth(server.token); 8 | } else if (server.username || server.password) { 9 | db.useBasicAuth(server.username, server.password); 10 | } 11 | return db; 12 | }; 13 | -------------------------------------------------------------------------------- /lib/util/fs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const extractZip = require("extract-zip"); 3 | const fs = require("fs"); 4 | const promisify = require("util.promisify"); 5 | const path = require("path"); 6 | 7 | const promisify2 = (fn) => (...args) => 8 | new Promise((resolve, reject) => { 9 | try { 10 | fn(...args, (result) => { 11 | resolve(result); 12 | }); 13 | } catch (e) { 14 | reject(e); 15 | } 16 | }); 17 | 18 | exports.extract = extractZip; 19 | exports.exists = promisify2(fs.exists); 20 | exports.mkdir = promisify(fs.mkdir); 21 | exports.readdir = promisify(fs.readdir); 22 | exports.readFile = promisify(fs.readFile); 23 | exports.stat = promisify(fs.stat); 24 | exports.unlink = promisify(fs.unlink); 25 | exports.writeFile = promisify(fs.writeFile); 26 | exports.realpath = promisify(fs.realpath); 27 | 28 | exports.safeStat = async function safeStat(path) { 29 | try { 30 | const stats = await exports.stat(path); 31 | return stats; 32 | } catch (e) { 33 | return null; 34 | } 35 | }; 36 | 37 | exports.walk = async function walk(basepath, shouldIgnore) { 38 | const followed = [await exports.realpath(basepath)]; 39 | const dirs = [basepath]; 40 | const files = []; 41 | for (const dirpath of dirs) { 42 | const names = await exports.readdir(dirpath); 43 | await Promise.all( 44 | names.map(async (name) => { 45 | const abspath = path.join(dirpath, name); 46 | const stats = await exports.safeStat(abspath); 47 | if (stats) { 48 | if (stats.isDirectory()) { 49 | const realpath = await exports.realpath(abspath); 50 | if (realpath !== abspath) { 51 | if (followed.includes(realpath)) return; 52 | followed.push(realpath); 53 | } 54 | dirs.push(abspath); 55 | } else if (stats.isFile()) { 56 | const relpath = path.relative(basepath, abspath); 57 | if (shouldIgnore && shouldIgnore(relpath)) return; 58 | files.push(relpath); 59 | } 60 | } 61 | }) 62 | ); 63 | } 64 | return files; 65 | }; 66 | -------------------------------------------------------------------------------- /lib/util/log.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-console */ 2 | "use strict"; 3 | const { red, yellow, bold, gray } = require("chalk"); 4 | const { format, isError } = require("util"); 5 | const packageJson = require("../../package.json"); 6 | 7 | const bugsUrl = packageJson.bugs.url; 8 | 9 | exports.indentable = function indentable(start = 0) { 10 | return { 11 | level: start, 12 | log(...messages) { 13 | if (!messages.length) console.log(); 14 | else if (this.level <= 0) console.log(...messages); 15 | else console.log(" ".repeat(this.level * 2 - 1), ...messages); 16 | }, 17 | indent(level = 1) { 18 | this.level += level; 19 | }, 20 | dedent(level = 1) { 21 | this.level -= level; 22 | }, 23 | }; 24 | }; 25 | 26 | exports.info = function info(message) { 27 | console.info(message); 28 | }; 29 | 30 | exports.detail = function detail(message) { 31 | console.info(gray(message)); 32 | }; 33 | 34 | exports.json = function json(obj) { 35 | console.info(JSON.stringify(obj, null, 2)); 36 | }; 37 | 38 | exports.warn = function warn(message) { 39 | if (isError(message)) message = message.stack || message.message || message; 40 | console.error(yellow(format(message))); 41 | }; 42 | 43 | exports.error = function error(message) { 44 | if (isError(message)) message = message.stack || message.message || message; 45 | console.error(red(format(message))); 46 | }; 47 | 48 | exports.fatal = function fatal(err) { 49 | if (typeof err === "string") { 50 | exports.error(err); 51 | } else if (err.code === "ENETUNREACH") { 52 | exports.error( 53 | `Network unreachable: ${red.bold(err.address)}:${red.bold( 54 | err.port 55 | )}\nThis indicates connectivity issues or a server problem. Are you offline?` 56 | ); 57 | } else if (err.code === "EHOSTUNREACH") { 58 | exports.error( 59 | `Host unreachable: ${red.bold(err.address)}${ 60 | err.port ? `:${red.bold(err.port)}` : "" 61 | }\nThis indicates connectivity issues or a server problem. Is the server accessible from this network?` 62 | ); 63 | } else if (err.code === "ECONNREFUSED") { 64 | exports.error( 65 | `Connection refused: ${red.bold(err.address)}${ 66 | err.port ? `:${red.bold(err.port)}` : "" 67 | }\nThis indicates connectivity issues or a server problem. Is the server down?` 68 | ); 69 | } else if (err.code === "ECONNRESET") { 70 | exports.error( 71 | `Connection reset by peer. The server closed the connection unexpectedly.\nThis indicates connectivity issues or a server problem.` 72 | ); 73 | } else if (err.code === "EMFILE") { 74 | exports.error( 75 | `Too many open files. Your operating system has reached the maximum number of open file descriptors. Try running ${bold( 76 | "`ulimit -n 2048`" 77 | )} and try again.` 78 | ); 79 | } else if (err.code === "EPIPE") { 80 | exports.error( 81 | `Broken pipe. Connection was dropped during upload.\nThis indicates connectivity issues or a server problem.` 82 | ); 83 | } else if (err.code === "ETIMEDOUT") { 84 | exports.error( 85 | `Operation timed out. Server is not responding.\nThis indicates connectivity issues or a server problem.` 86 | ); 87 | } else if ( 88 | typeof err.code === "string" && 89 | err.code.match(/^E[A-Z]+$/) && 90 | !err.errorNum 91 | ) { 92 | exports.error( 93 | `Unknown system error:\n\n${bold( 94 | format(err.stack || err.message || err) 95 | )}\n\n\nThis may indicate connectivity issues or a server problem. See the list of error names in the errno(3) man page for more information: ${bold( 96 | "http://man7.org/linux/man-pages/man3/errno.3.html" 97 | )}\n\nIf you believe this to be an bug in ${bold( 98 | "foxx-cli" 99 | )} please open an issue at ${bold( 100 | bugsUrl 101 | )} with a full copy of the error message and a description of what you were trying to do when this problem occurred.` 102 | ); 103 | } else if (err.isArangoError) { 104 | if (err.errorNum === 11) { 105 | exports.error( 106 | `Server refused authorization.\nEither your credentials are invalid or the user has insufficient privileges.` 107 | ); 108 | } else { 109 | exports.error( 110 | `Unexpected ArangoDB error (Code: ${err.errorNum || "?"}):\n${ 111 | err.message 112 | }` 113 | ); 114 | } 115 | } else if (err.statusCode === 401) { 116 | exports.error("Authentication failed. Bad username or password?"); 117 | } else if (typeof err.statusCode === "number") { 118 | exports.error( 119 | `The server responded with a ${bold(err.statusCode)} status code.\n${ 120 | err.statusCode >= 500 121 | ? "This typically indicates a server-side error." 122 | : "This typically indicates a problem with the request." 123 | }\nPlease check the ArangoDB log file to determine the cause of this error.\n\nIf you believe this to be an bug in ${bold( 124 | "foxx-cli" 125 | )} please open an issue at ${bold( 126 | bugsUrl 127 | )} with the relevant part of the ArangoDB log and a description of what you were trying to do when this problem occurred.\n\nWe apologize for the inconvenience.` 128 | ); 129 | } else { 130 | exports.error( 131 | `Sorry! An unexpected error occurred. This is likely a bug in ${bold( 132 | "foxx-cli" 133 | )}.\nPlease open an issue at ${bold( 134 | bugsUrl 135 | )} with a full copy of the following error message and a description of what you were trying to do when this problem occurred.\n\n${bold( 136 | format(err.stack || err.message || err) 137 | )}\n\nWe apologize for the inconvenience.` 138 | ); 139 | } 140 | process.exit(1); 141 | }; 142 | -------------------------------------------------------------------------------- /lib/util/parseOptions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { splat } = require("./array"); 3 | 4 | module.exports = function parseOptions(options) { 5 | if (!options || !options.length) return null; 6 | if (options.length === 1 && options[0] === "@") { 7 | return options[0]; 8 | } 9 | const parsed = {}; 10 | for (const pair of splat(options)) { 11 | const [key, ...tail] = pair.split("="); 12 | const value = tail.join("="); 13 | parsed[key] = value; 14 | } 15 | return parsed; 16 | }; 17 | -------------------------------------------------------------------------------- /lib/util/streamToBuffer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = function streamToBuffer(stream) { 3 | return new Promise((resolve, reject) => { 4 | const chunks = []; 5 | stream.on("error", (err) => reject(err)); 6 | stream.on("data", (chunk) => { 7 | chunks.push(chunk); 8 | }); 9 | stream.on("close", () => resolve(Buffer.concat(chunks))); 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/util/text.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const cliui = require("cliui"); 3 | const yargs = require("yargs"); 4 | 5 | exports.group = function group(title, ...args) { 6 | if (Array.isArray(title)) { 7 | args.unshift(title); 8 | title = undefined; 9 | } 10 | const wrapWidth = Math.max(80, Math.min(160, yargs.terminalWidth())); 11 | const ui = cliui({ width: wrapWidth, wrap: true }); 12 | const maxLength = args.reduce( 13 | (base, [name]) => Math.max(base, name.length), 14 | 0 15 | ); 16 | const leftWidth = Math.min(maxLength, Math.floor(wrapWidth / 2)); 17 | if (title) ui.div(title); 18 | for (const [name, desc, extra] of args) { 19 | ui.span({ text: name, padding: [0, 2, 0, 2], width: leftWidth + 4 }, desc); 20 | if (extra) { 21 | ui.div({ text: extra, padding: [0, 0, 0, 2], align: "right" }); 22 | } else { 23 | ui.div(); 24 | } 25 | } 26 | return ui.toString(); 27 | }; 28 | 29 | exports.comma = function comma(arr, and = "and") { 30 | if (!arr.length) return ""; 31 | if (arr.length === 1) return arr[0]; 32 | return `${arr.slice(0, arr.length - 1).join(", ")} ${and} ${ 33 | arr[arr.length - 1] 34 | }`; 35 | }; 36 | 37 | exports.inline = function inline(strings, ...values) { 38 | const strb = [strings[0]]; 39 | for (let i = 0; i < values.length; i++) { 40 | strb.push(values[i], strings[i + 1]); 41 | } 42 | return strb 43 | .join("") 44 | .replace(/([ \t]+\n|\n[ \t]+)/g, "\n") 45 | .replace(/\n\n+/g, (match) => match.replace(/\n/g, "\0")) 46 | .replace(/\n/g, " ") 47 | .replace(/\0/g, "\n") 48 | .replace(/(^\s|\s$)/g, ""); 49 | }; 50 | 51 | exports.mask = function mask(val) { 52 | const str = String(val); 53 | return str.replace(/./g, "*"); 54 | }; 55 | -------------------------------------------------------------------------------- /lib/util/zip.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { createReadStream, createWriteStream } = require("fs"); 3 | const { resolve: resolvePath } = require("path"); 4 | 5 | const archiver = require("archiver"); 6 | const { createWriteStream: createTempStream } = require("temp"); 7 | const temp = require("temp"); 8 | const { version } = require("../../package.json"); 9 | const fs = require("./fs"); 10 | 11 | const comment = `Created with foxx-cli v${version} (https://foxx.arangodb.com)`; 12 | 13 | function append(zipstream, basepath, name) { 14 | return new Promise((resolve, reject) => { 15 | const stream = createReadStream(resolvePath(basepath, name)); 16 | stream.on("error", (e) => reject(e)); 17 | stream.on("close", () => resolve()); 18 | zipstream.append(stream, { name }); 19 | }); 20 | } 21 | 22 | exports.zip = function zip(basepath, files, dest) { 23 | return new Promise(async (resolve, reject) => { 24 | let filename, filestream; 25 | if (typeof dest === "string") { 26 | filename = dest; 27 | filestream = createWriteStream(dest); 28 | } else if (dest) { 29 | filestream = dest; 30 | filename = dest.path; 31 | } else { 32 | filestream = createTempStream({ suffix: ".zip" }); 33 | filename = filestream.path; 34 | } 35 | filestream.on("close", () => resolve(filename)); 36 | filestream.on("error", (e) => reject(e)); 37 | const zipstream = archiver("zip", { comment }); 38 | zipstream.on("error", (e) => reject(e)); 39 | zipstream.pipe(filestream); 40 | for (const name of files) { 41 | await append(zipstream, basepath, name); 42 | } 43 | zipstream.finalize(); 44 | }); 45 | }; 46 | 47 | exports.extractBuffer = async function extractBuffer(buf, ...args) { 48 | const tmpfile = temp.path({ suffix: ".zip" }); 49 | try { 50 | await fs.writeFile(tmpfile, buf); 51 | await fs.extract(tmpfile, ...args); 52 | } finally { 53 | await fs.unlink(tmpfile); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foxx-cli", 3 | "version": "2.1.1", 4 | "description": "CLI for ArangoDB Foxx.", 5 | "preferGlobal": true, 6 | "bin": { 7 | "foxx": "bin/foxx" 8 | }, 9 | "engines": { 10 | "node": ">=12" 11 | }, 12 | "files": [ 13 | "bin/", 14 | "lib/", 15 | "templates/", 16 | "README.md", 17 | "LICENSE" 18 | ], 19 | "scripts": { 20 | "test": "mocha --reporter spec --require source-map-support/register --timeout 10000 lib/test", 21 | "ci": "mocha --reporter spec --require source-map-support/register --timeout 10000 lib/test", 22 | "preci": "yarn install" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/arangodb/foxx-cli.git" 27 | }, 28 | "author": "Alan Plum ", 29 | "license": "Apache-2.0", 30 | "bugs": { 31 | "url": "https://github.com/arangodb/foxx-cli/issues" 32 | }, 33 | "homepage": "https://github.com/arangodb/foxx-cli#readme", 34 | "dependencies": { 35 | "arangojs": "^8.0.0", 36 | "archiver": "^5.3.0", 37 | "chalk": "^4.1.0", 38 | "cliui": "^7.0.4", 39 | "dedent": "^0.7.0", 40 | "ejs": "^3.1.3", 41 | "extract-zip": "^2.0.1", 42 | "got": "^11.8.3", 43 | "i": "^0.3.6", 44 | "ini": "^3.0.1", 45 | "inquirer": "^8.0.0", 46 | "lodash": "^4.17.4", 47 | "minimatch": "^3.0.4", 48 | "semver": "^7.3.2", 49 | "spdx-license-list": "^6.0.0", 50 | "temp": "^0.9.0", 51 | "util.promisify": "^1.0.0", 52 | "yargs": "^16.0.0" 53 | }, 54 | "devDependencies": { 55 | "babel-eslint": "^10.1.0", 56 | "chai": "^4.1.2", 57 | "eslint": "^8.7.0", 58 | "eslint-config-prettier": "^8.3.0", 59 | "eslint-plugin-prettier": "^4.0.0", 60 | "mocha": "^10.1.0", 61 | "prettier": "^2.0.5", 62 | "source-map-support": "^0.5.4" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /templates/LICENSE.ejs: -------------------------------------------------------------------------------- 1 | Copyright (c) <%- 2 | new Date().getFullYear() 3 | %> <%- 4 | typeof author !== 'undefined' ? author : '' 5 | %>. All rights reserved. 6 | -------------------------------------------------------------------------------- /templates/README.md.ejs: -------------------------------------------------------------------------------- 1 | # <%- 2 | name 3 | -%> 4 | 5 | <%- 6 | typeof description !== 'undefined' 7 | ? '\n' + description + '\n' 8 | : '' 9 | -%> 10 | <% 11 | if (typeof provides !== 'undefined') { 12 | %> 13 | This service provides the following Foxx dependencies: 14 | <% 15 | Object.keys(provides).sort().forEach(function (key) { 16 | %> 17 | * **<%- 18 | key 19 | %>**: `<%- 20 | provides[key] 21 | %>` 22 | <% 23 | }); 24 | %> 25 | <% 26 | } 27 | -%> 28 | <% 29 | if (typeof configuration !== 'undefined') { 30 | -%> 31 | 32 | ## Configuration 33 | 34 | This service has the following configuration options: 35 | 36 | <% 37 | Object.keys(configuration).sort().forEach(function (key) { 38 | var cfg = configuration[key]; 39 | -%> 40 | * **<%- 41 | key 42 | %>**: `<%- 43 | cfg.type 44 | %>`<%- 45 | cfg.default 46 | ? ' (Default: `' + cfg.default + '`)' 47 | : (cfg.required ? '' : ' (optional)') 48 | %> 49 | <%- 50 | cfg.description 51 | ? '\n ' + cfg.description.replace(/\n/g, '\n ') + '\n' 52 | : '' 53 | %> 54 | <% 55 | }); 56 | -%> 57 | <% 58 | } 59 | -%> 60 | <% 61 | if (typeof dependencies !== 'undefined') { 62 | -%> 63 | 64 | ## Dependencies 65 | 66 | This service uses the following Foxx dependencies: 67 | 68 | <% 69 | Object.keys(dependencies).sort().forEach(function (key) { 70 | var dep = dependencies[key]; 71 | -%> 72 | * **<%- 73 | dep.alias 74 | %>**: `<%- 75 | dep.name 76 | %>:<%- 77 | dep.version 78 | %>`<%- 79 | typeof dep.multiple !== 'undefined' 80 | ? ' (multiple)' 81 | : '' 82 | %><%- 83 | typeof dep.required !== 'undefined' 84 | ? '' 85 | : ' (optional)' 86 | %> 87 | <%- 88 | typeof dep.description !== 'undefined' 89 | ? '\n ' + dep.description.replace(/\n/g, '\n ') + '\n' 90 | : '' 91 | %> 92 | <% 93 | }); 94 | -%> 95 | <% 96 | } 97 | -%> 98 | 99 | ## License 100 | <% 101 | if (typeof license !== 'undefined' && license) { 102 | %> 103 | The <%- 104 | license 105 | %> license. For more information, see the accompanying LICENSE file. 106 | <% 107 | } else { 108 | %> 109 | Copyright (c) <%- 110 | new Date().getFullYear() 111 | %> <%- 112 | typeof authorName !== 'undefined' && authorName ? authorName : '' 113 | %>. All rights reserved. 114 | <% 115 | } 116 | -%> 117 | -------------------------------------------------------------------------------- /templates/crud.js.ejs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const dd = require('dedent'); 3 | const joi = require('joi'); 4 | const errors = require('@arangodb').errors; 5 | const createRouter = require('@arangodb/foxx/router'); 6 | <% if (!prefixed) { -%> 7 | const db = require('@arangodb').db; 8 | <% } -%> 9 | 10 | const <%= singular %>Schema = { 11 | _key: joi.string(), 12 | // Describe the attributes for outgoing <%= plural %> 13 | // e.g. age: joi.number().integer() 14 | }; 15 | 16 | const <%= singular %>IncomingSchema = { 17 | <% if (isEdgeCollection) { -%> 18 | _from: joi.string(), 19 | _to: joi.string(), 20 | <% } -%> 21 | // Describe the attributes for incoming <%= plural %> 22 | }; 23 | 24 | const <%= singular %>PatchSchema = { 25 | // Describe the attributes the patch route should accept here 26 | }; 27 | 28 | const <%= plural %> = <%= 29 | prefixed 30 | ? "module.context.collection" 31 | : "db._collection" 32 | %>('<%= collection %>'); 33 | const keySchema = joi.string().required() 34 | .description('The key of the <%= singular %>'); 35 | 36 | const ARANGO_NOT_FOUND = errors.ERROR_ARANGO_DOCUMENT_NOT_FOUND.code; 37 | const ARANGO_DUPLICATE = errors.ERROR_ARANGO_UNIQUE_CONSTRAINT_VIOLATED.code; 38 | const ARANGO_CONFLICT = errors.ERROR_ARANGO_CONFLICT.code; 39 | 40 | const router = createRouter(); 41 | module.exports = router; 42 | 43 | router.get(function (req, res) { 44 | res.send(<%= plural %>.all()); 45 | }, 'list') 46 | .response([<%= singular %>Schema], 'A list of <%= plural %>.') 47 | .summary('List all <%= plural %>') 48 | .description(dd` 49 | Retrieves a list of all <%= plural %>. 50 | `); 51 | 52 | router.post(function (req, res) { 53 | const <%= singular %> = req.body; 54 | let meta; 55 | try { 56 | meta = <%= plural %>.save(<%= singular %>); 57 | } catch (e) { 58 | if (e.isArangoError && e.errorNum === ARANGO_DUPLICATE) { 59 | res.throw('conflict', e.message); 60 | } 61 | res.throw(e); 62 | } 63 | Object.assign(<%= singular %>, meta); 64 | res.status(201); 65 | res.set('location', req.makeAbsolute( 66 | req.reverse('detail', {key: <%= singular %>._key}) 67 | )); 68 | res.send(<%= singular %>); 69 | }, 'create') 70 | .body(<%= singular %>IncomingSchema, 'The <%= singular %> to create.') 71 | .response(201, <%= singular %>Schema, 'The created <%= singular %>.') 72 | .error('conflict', 'The <%= singular %> already exists.') 73 | .summary('Create a new <%= singular %>') 74 | .description(dd` 75 | Creates a new <%= singular %> from the request body and 76 | returns the saved <%= singular %>. 77 | `); 78 | 79 | router.get(':key', function (req, res) { 80 | const key = req.pathParams.key; 81 | let <%= singular %> 82 | try { 83 | <%= singular %> = <%= plural %>.document(key); 84 | } catch (e) { 85 | if (e.isArangoError && e.errorNum === ARANGO_NOT_FOUND) { 86 | res.throw('not found', e.message); 87 | } 88 | res.throw(e); 89 | } 90 | res.send(<%= singular %>); 91 | }, 'detail') 92 | .pathParam('key', keySchema) 93 | .response(<%= singular %>Schema, 'The <%= singular %>.') 94 | .summary('Fetch a <%= singular %>') 95 | .description(dd` 96 | Retrieves a <%= singular %> by its key. 97 | `); 98 | 99 | router.put(':key', function (req, res) { 100 | const key = req.pathParams.key; 101 | const <%= singular %> = req.body; 102 | let meta; 103 | try { 104 | meta = <%= plural %>.replace(key, <%= singular %>); 105 | } catch (e) { 106 | if (e.isArangoError) { 107 | if (e.errorNum === ARANGO_NOT_FOUND) { 108 | res.throw('not found', e.message); 109 | } else if (e.errorNum === ARANGO_CONFLICT) { 110 | res.throw('conflict', e.message); 111 | } 112 | } 113 | res.throw(e); 114 | } 115 | Object.assign(<%= singular %>, meta); 116 | res.send(<%= singular %>); 117 | }, 'replace') 118 | .pathParam('key', keySchema) 119 | .body(<%= singular %>IncomingSchema, 'The data to replace the <%= singular %> with.') 120 | .response(<%= singular %>Schema, 'The new <%= singular %>.') 121 | .summary('Replace a <%= singular %>') 122 | .description(dd` 123 | Replaces an existing <%= singular %> with the request body and 124 | returns the new <%= singular %>. 125 | `); 126 | 127 | router.patch(':key', function (req, res) { 128 | const key = req.pathParams.key; 129 | const patchData = req.body; 130 | let <%= singular %>; 131 | try { 132 | <%= plural %>.update(key, patchData); 133 | <%= singular %> = <%= plural %>.document(key); 134 | } catch (e) { 135 | if (e.isArangoError) { 136 | if (e.errorNum === ARANGO_NOT_FOUND) { 137 | res.throw('not found', e.message); 138 | } 139 | if (e.errorNum === ARANGO_CONFLICT) { 140 | res.throw('conflict', e.message); 141 | } 142 | } 143 | res.throw(e); 144 | } 145 | res.send(<%= singular %>); 146 | }, 'update') 147 | .pathParam('key', keySchema) 148 | .body(<%= singular %>PatchSchema, 'The data to update the <%= singular %> with.') 149 | .response(<%= singular %>Schema, 'The updated <%= singular %>.') 150 | .summary('Update a <%= singular %>') 151 | .description(dd` 152 | Patches a <%= singular %> with the request body and 153 | returns the updated document. 154 | `); 155 | 156 | router.delete(':key', function (req, res) { 157 | const key = req.pathParams.key; 158 | try { 159 | <%= plural %>.remove(key); 160 | } catch (e) { 161 | if (e.isArangoError && e.errorNum === ARANGO_NOT_FOUND) { 162 | res.throw('not found', e.message); 163 | } 164 | res.throw(e); 165 | } 166 | }, 'delete') 167 | .pathParam('key', keySchema) 168 | .response(null) 169 | .summary('Remove a <%= singular %>') 170 | .description(dd` 171 | Deletes a <%= singular %> from the database. 172 | `); 173 | -------------------------------------------------------------------------------- /templates/example/index.js.ejs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const createRouter = require('@arangodb/foxx/router'); 3 | 4 | const router = createRouter(); 5 | module.context.use(router); 6 | 7 | router.get('/', (req, res) => { 8 | res.write('Hello World!') 9 | }) 10 | .response(['text/plain']); 11 | -------------------------------------------------------------------------------- /templates/index.js.ejs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | <% if (typeof generateCrudRoutes !== 'undefined' && generateCrudRoutes && typeof documentCollections !== 'undefined') { 4 | documentCollections.forEach(function (collection) { -%> 5 | module.context.use('/<%- collection %>', require('./api/<%- collection %>'), '<%- collection %>'); 6 | <% }) 7 | } -%> 8 | <% if (typeof generateCrudRoutes !== 'undefined' && generateCrudRoutes && typeof edgeCollections !== 'undefined') { 9 | edgeCollections.forEach(function (collection) { -%> 10 | module.context.use('/<%- collection %>', require('./api/<%- collection %>'), '<%- collection %>'); 11 | <% }) 12 | } -%> 13 | -------------------------------------------------------------------------------- /templates/router.js.ejs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const joi = require('joi'); 3 | const createRouter = require('@arangodb/foxx/router'); 4 | 5 | const router = createRouter(); 6 | module.exports = router; 7 | 8 | router.use((req, res, next) => { 9 | // Manipulate req here before it is handled by a route 10 | // e.g. req.session = {} 11 | next(); 12 | // Manipulate res here after it was handled by a route 13 | // e.g. res.set('x-session-id', 'keyboardcat') 14 | }); 15 | 16 | router.post('/', (req, res) => { 17 | // Handle the request and generate the response 18 | }) 19 | .body(joi.object(), 'the request body') 20 | .response(200, joi.object(), 'the response body'); 21 | -------------------------------------------------------------------------------- /templates/script.js.ejs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const db = require('@arangodb').db; 3 | const args = module.context.argv; 4 | 5 | // module.exports = "script result"; 6 | -------------------------------------------------------------------------------- /templates/setup.js.ejs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const db = require('@arangodb').db; 3 | const documentCollections = <%- 4 | JSON.stringify(documentCollections.sort(), null, 2) 5 | %>; 6 | const edgeCollections = <%- 7 | JSON.stringify(edgeCollections.sort(), null, 2) 8 | %>; 9 | 10 | for (const name of documentCollections) { 11 | if (!module.context.collection(name)) { 12 | const prefixedName = module.context.collectionName(name); 13 | db._createDocumentCollection(prefixedName); 14 | } 15 | } 16 | 17 | for (const name of edgeCollections) { 18 | if (!module.context.collection(name)) { 19 | const prefixedName = module.context.collectionName(name); 20 | db._createEdgeCollection(prefixedName); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /templates/teardown.js.ejs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const db = require('@arangodb').db; 3 | const allCollections = <%- 4 | JSON.stringify([ 5 | ...documentCollections.sort(), 6 | ...edgeCollections.sort() 7 | ], null, 2) 8 | %>; 9 | 10 | for (const name of allCollections) { 11 | const prefixedName = module.context.collectionName(name); 12 | db._drop(prefixedName); 13 | } 14 | -------------------------------------------------------------------------------- /templates/test.js.ejs: -------------------------------------------------------------------------------- 1 | /*global describe, it, before, after, beforeEach, afterEach */ 2 | 'use strict'; 3 | const expect = require('chai').expect; 4 | 5 | describe('test suite', () => { 6 | it('contains a test case', () => { 7 | expect(true).not.to.equal(false); 8 | }); 9 | }); 10 | --------------------------------------------------------------------------------