├── docker-compose.yaml ├── package.json ├── LICENSE ├── .gitignore ├── README.md ├── src ├── metabase.js └── metabase-async-await.js └── app.js /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | source: 4 | image: mafs/metabase-custom 5 | ports: 6 | - 3000:3000 7 | destination: 8 | image: mafs/metabase-custom 9 | ports: 10 | - 3001:3000 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metabase-migration", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/itmi-id/metabase-migration.git" 12 | }, 13 | "author": "Luthfi Hariz", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/itmi-id/metabase-migration/issues" 17 | }, 18 | "homepage": "https://github.com/itmi-id/metabase-migration#readme", 19 | "dependencies": { 20 | "axios": "^0.21.0", 21 | "dotenv": "^8.2.0", 22 | "postman-request": "^2.88.1-postman.23", 23 | "yargs": "^15.4.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Indopasifik Teknologi Medika Indonesia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![version](https://img.shields.io/github/v/tag/itmi-id/metabase-migration?label=latest%20version) 2 | 3 | # Metabase Migration 4 | Script to update and duplicate Question in Metabase using Metabase REST API. 5 | 6 | Here is a blog post on how we approach managing multiple environments in Embedded Metabase https://medium.com/itmi-engineering/managing-multiple-environments-with-embedded-metabase-87b074ea9aad 7 | 8 | ## How to use 9 | Clone this repository and `npm install` 10 | 11 | Create .env file in root folder that contains 12 | 13 | ```sh 14 | METABASE_BASE_URL=xxx 15 | METABASE_USERNAME=xxx 16 | METABASE_PASSWORD=xxx 17 | 18 | # Optionaly, when performing actions on the source Metabase instance above to a destination/second Metabase instance, add the following 19 | DESTINATION_METABASE_BASE_URL=xxx 20 | DESTINATION_METABASE_USERNAME=xxx 21 | DESTINATION_METABASE_PASSWORD=xxx 22 | ``` 23 | 24 | ### Update Question 25 | 26 | `node app.js update —-originId=[question_id] --destId=[question_id] —-databaseId=[database_id]` 27 | 28 | ### Duplicate Question 29 | 30 | `node app.js duplicate —-questionId=[question_id] --collectionId=[collection_id] -—name=[name] -—databaseId=[database_id]` 31 | 32 | `--name` is optional, if it's not provided it will use the same question name. 33 | 34 | ### Duplicate Question on Destination Instance 35 | 36 | `node app.js duplicateAcross —-questionId=[question_id] --collectionId=[collection_id] -—name=[name] -—databaseId=[database_id]` 37 | 38 | `--name` is optional, if it's not provided it will use the same question name. 39 | 40 | ## Work in Progress 41 | 42 | - Duplicating or updating question between different metabase instance 43 | - Duplicating or updating Dashboard 44 | 45 | 46 | ## Testing with Custom Metabase image just past initial setup 47 | 48 | ** Note: this container should be used for testing purposes only!!! ** 49 | 50 | Use the image already generated using the steps below (assumed to be using in the `docker-compose.yaml` file): 51 | 52 | ```sh 53 | docker-compose up 54 | ``` 55 | 56 | You should then have a source `localhost:3000` and destination `localhost:3001` container to execute commands against. Add the following test `.env` file to your project to use them: 57 | 58 | ```sh 59 | # source instance 60 | METABASE_BASE_URL=http://localhost:3000/api 61 | METABASE_USERNAME=test@test.com 62 | METABASE_PASSWORD=test1234 63 | 64 | # destination instance 65 | DESTINATION_METABASE_BASE_URL=http://localhost:3001/api 66 | DESTINATION_METABASE_USERNAME=test@test.com 67 | DESTINATION_METABASE_PASSWORD=test1234 68 | ``` 69 | 70 | Sanity check by running the following: `node app.js duplicateAcross --questionId=6 --collectionId=2 --name="testing coordinates copy across instances" --databaseId=1` 71 | 72 | If successful, you should be able to view the question @ [localhost:3001/collection/2](http://localhost:3001/collection/2) 73 | 74 | ### Steps to generate a post install metabse image 75 | 76 | The following are the steps followed to generate the image used in the `docker-compose.yaml`: 77 | 78 | 1. From cli, run `docker run -it -p 3000:3000 --name metabase metabase/metabase[:TAG]` 79 | 1. Once container is launched, visit [localhost:3000/setup](http://localhost:3000/setup) 80 | 1. Click `Let's get started` 81 | 1. Select `English` as preferred language 82 | 1. Enter the following: 83 | * First name: `test` 84 | * Last name: `test` 85 | * Email: `test@test.com` 86 | * Create a password: `test1234` 87 | * Your company or team name: `test` 88 | 1. Click `Next` 89 | 1. Select `I'll add my data later` 90 | 1. Click `Next` 91 | 1. Click `Take me to Metabase` 92 | 1. Under the `TRY THESE X-RAYS BASED ON YOUR DATA.` section, click `A look at your People table` 93 | 1. Click `Save this` 94 | 1. Verify that you have a new collection (id 2 in URL) called `Automatically Generated Dashboards` 95 | 1. Verify that you have a new collection (id 3 in URL) called `A look at your People table` 96 | 1. Back on the cli, run the following to generate a snapshot of the image: `docker commit metabase mafs/metabase-custom[:TAG]` 97 | 1. Push your image to dockerhub: `docker push mafs/metabase-custom[:TAG]` 98 | 1. Launch 2 instances (a source and destination instance) to run tests against containers: `docker-compose up` 99 | -------------------------------------------------------------------------------- /src/metabase.js: -------------------------------------------------------------------------------- 1 | const request = require('postman-request') 2 | 3 | require('dotenv').config() 4 | 5 | const baseUrl = process.env.METABASE_BASE_URL 6 | const username = process.env.METABASE_USERNAME 7 | const password = process.env.METABASE_PASSWORD 8 | 9 | const auth = (callback) => { 10 | const url = baseUrl + "/session" 11 | console.log("Authenticating",username) 12 | request({url, json:true, method:"POST", body: {username: username, password:password}}, (error, response) => { 13 | if (error) { 14 | callback(error, undefined) 15 | } else { 16 | token = response.body.id 17 | callback(undefined, response.body.id) 18 | } 19 | }) 20 | } 21 | 22 | const update = (origin, destination, databaseId, callback) => { 23 | getQuestion(origin, (error, response) => { 24 | 25 | if (error) { 26 | callback(error, undefined) 27 | return 28 | } 29 | 30 | const {visualization_settings, description, collection_position, result_metadata, dataset_query, display} = response.body 31 | dataset_query.database = databaseId 32 | const body = { 33 | visualization_settings, 34 | description, 35 | result_metadata, 36 | dataset_query, 37 | display 38 | } 39 | updateQuestion(destination, body, (error, response) => { 40 | if (error){ 41 | callback(error, undefined) 42 | return 43 | } 44 | 45 | callback(undefined, response) 46 | }) 47 | }) 48 | } 49 | 50 | const updateQuestion = (id, body, callback) => { 51 | console.log("Updating question...") 52 | const url = baseUrl + "/card/" + id 53 | request({ url, json:true, headers: {"X-Metabase-Session": token}, method:"PUT", body}, (error, response) => { 54 | if (error){ 55 | callback(error, undefined) 56 | return 57 | } 58 | 59 | callback(undefined, response) 60 | }) 61 | } 62 | 63 | const duplicate = (questionId, collectionId, questionName, databaseId, callback) => { 64 | getQuestion(questionId, (error, response) => { 65 | 66 | if (error) { 67 | callback(error, undefined) 68 | return 69 | } 70 | 71 | const {visualization_settings, description, collection_position, result_metadata, dataset_query, display} = response.body 72 | dataset_query.database = databaseId 73 | 74 | var name = questionName 75 | if (!name) { 76 | name = response.body.name 77 | } 78 | 79 | const body = { 80 | visualization_settings, 81 | description, 82 | collection_id: collectionId, 83 | collection_position, 84 | result_metadata, 85 | dataset_query, 86 | name, 87 | display 88 | } 89 | 90 | createQuestion(body, (error, response) => { 91 | if (error){ 92 | callback(error, undefined) 93 | return 94 | } 95 | 96 | callback(undefined, response) 97 | }) 98 | }) 99 | } 100 | 101 | const createQuestion = (body, callback) => { 102 | console.log("Creating new question...") 103 | console.log(body) 104 | const url = baseUrl + "/card/" 105 | request({ url, json:true, headers: {"X-Metabase-Session": token}, method:"POST", body}, (error, response) => { 106 | if (error){ 107 | callback(error, undefined) 108 | return 109 | } 110 | 111 | callback(undefined, response) 112 | }) 113 | } 114 | 115 | const getQuestion = (id, callback) => { 116 | auth((error, token) => { 117 | if (error) { 118 | callback(error) 119 | return 120 | } 121 | 122 | console.log("Retrieving question id",id) 123 | 124 | const url = baseUrl + "/card/" + id 125 | request({url, json:true, headers:{"X-Metabase-Session": token}}, (error, response) => { 126 | if (error){ 127 | callback(error, undefined) 128 | return 129 | } 130 | 131 | callback(undefined, response) 132 | }) 133 | 134 | }) 135 | } 136 | 137 | 138 | 139 | module.exports = { 140 | update: update, 141 | duplicate: duplicate 142 | } -------------------------------------------------------------------------------- /src/metabase-async-await.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | 3 | require('dotenv').config() 4 | 5 | const baseUrl = process.env.METABASE_BASE_URL 6 | const username = process.env.METABASE_USERNAME 7 | const password = process.env.METABASE_PASSWORD 8 | 9 | const { 10 | DESTINATION_METABASE_BASE_URL, 11 | DESTINATION_METABASE_USERNAME, 12 | DESTINATION_METABASE_PASSWORD 13 | } = process.env; 14 | 15 | async function update(originQuestionId, destinationQuestionId, destinationDatabaseId) { 16 | console.log("Authenticating",username); 17 | const axiosConfig = await auth(); 18 | console.log("Successfully authenticated with token", axiosConfig.headers); 19 | 20 | console.log("Retrieving question id", originQuestionId); 21 | const {visualization_settings, description, enable_embedding, 22 | result_metadata, dataset_query, display, embedding_params, } = await getQuestion(originQuestionId, axiosConfig); 23 | 24 | dataset_query.database = destinationDatabaseId; 25 | const body = { 26 | visualization_settings, 27 | description, 28 | result_metadata, 29 | dataset_query, 30 | display, 31 | enable_embedding, 32 | embedding_params 33 | }; 34 | 35 | console.log("\nUpdating question id", destinationQuestionId); 36 | console.log(body) 37 | const url = baseUrl + "/card/" + destinationQuestionId; 38 | 39 | const response = await axios.put(url, body, axiosConfig); 40 | return response; 41 | } 42 | 43 | async function duplicate(questionId, collectionId, questionName, databaseId) { 44 | console.log("Authenticating",username); 45 | const axiosConfig = await auth(); 46 | console.log("Successfully authenticated with token", axiosConfig.headers); 47 | 48 | console.log("Retrieving question id", questionId); 49 | const {visualization_settings, description, enable_embedding, collection_position, 50 | result_metadata, dataset_query, display, embedding_params, name:oldName } = await getQuestion(questionId, axiosConfig); 51 | dataset_query.database = databaseId; 52 | 53 | var name = questionName; 54 | if (!name) { 55 | name = oldName; 56 | } 57 | 58 | const body = { 59 | visualization_settings, 60 | description, 61 | collection_id: collectionId, 62 | collection_position, 63 | result_metadata, 64 | dataset_query, 65 | name, 66 | display, 67 | enable_embedding, 68 | embedding_params 69 | }; 70 | console.log("\nCreating new question with payload..."); 71 | console.log(body); 72 | const url = baseUrl + "/card/"; 73 | 74 | const response = await axios.post(url, body, axiosConfig); 75 | return response; 76 | } 77 | 78 | async function duplicateAcross(questionId, collectionId, questionName, databaseId) { 79 | console.log("Authenticating",username); 80 | const axiosConfigSource = await auth(); 81 | console.log("Successfully authenticated to source with token", axiosConfigSource.headers); 82 | 83 | console.log("Authenticating",DESTINATION_METABASE_USERNAME); 84 | const axiosConfigDestination = await destinationAuth(); 85 | console.log("Successfully authenticated to destination with token", axiosConfigDestination.headers); 86 | 87 | console.log("From source retrieving question id", questionId); 88 | const {visualization_settings, description, enable_embedding, collection_position, 89 | result_metadata, dataset_query, display, embedding_params, name:oldName } = await getQuestion(questionId, axiosConfigSource); 90 | dataset_query.database = databaseId; 91 | 92 | const name = questionName ? questionName : oldName; 93 | 94 | const body = { 95 | visualization_settings, 96 | description: description ? description : null, 97 | collection_id: collectionId, 98 | collection_position, 99 | result_metadata, 100 | dataset_query, 101 | name, 102 | display, 103 | enable_embedding, 104 | embedding_params 105 | }; 106 | console.log("\nCreating new question on destination with payload..."); 107 | console.log(body); 108 | const url = DESTINATION_METABASE_BASE_URL + "/card/"; 109 | 110 | const response = await axios.post(url, body, axiosConfigDestination); 111 | return response; 112 | } 113 | 114 | async function destinationAuth() { 115 | try { 116 | const authResponse = await axios({ 117 | method: 'post', 118 | url: DESTINATION_METABASE_BASE_URL+ "/session", 119 | data: { 120 | username: DESTINATION_METABASE_USERNAME, 121 | password: DESTINATION_METABASE_PASSWORD 122 | } 123 | }); 124 | const token = authResponse.data.id; 125 | const axiosConfig = { 126 | headers: { 127 | "X-Metabase-Session": token 128 | } 129 | }; 130 | 131 | return axiosConfig; 132 | } catch (error) { 133 | console.log("error", error.response.status); 134 | return 135 | } 136 | } 137 | 138 | async function auth() { 139 | try { 140 | const authResponse = await axios({ 141 | method: 'post', 142 | url: baseUrl+ "/session", 143 | data: { 144 | username: username, 145 | password: password 146 | } 147 | }); 148 | const token = authResponse.data.id; 149 | const axiosConfig = { 150 | headers: { 151 | "X-Metabase-Session": token 152 | } 153 | }; 154 | 155 | return axiosConfig; 156 | } catch (error) { 157 | console.log("error", error.response.status); 158 | return 159 | } 160 | } 161 | 162 | async function getQuestion(id, axiosConfig) { 163 | try { 164 | const url = baseUrl + "/card/" + id; 165 | const questionResponse = await axios.get(url, axiosConfig); 166 | return questionResponse.data; 167 | } catch (error) { 168 | console.log("Error retrieving question", error.response.status); 169 | } 170 | } 171 | 172 | 173 | module.exports = { 174 | update, duplicate, duplicateAcross 175 | } -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const yargs = require('yargs') 2 | const metabase = require('./src/metabase.js') 3 | const metabase_async_await = require('./src/metabase-async-await.js') 4 | 5 | yargs.command({ 6 | command: 'update', 7 | describe: 'Update question', 8 | builder: { 9 | originId: { 10 | describe: 'Origin question', 11 | demandOption: true, 12 | type: 'number' 13 | }, 14 | destId: { 15 | describe: 'Destination question', 16 | demandOption: true, 17 | type: 'number' 18 | }, 19 | databaseId: { 20 | describe: 'Database ID for destination question', 21 | demandOption: true, 22 | type: 'number' 23 | } 24 | }, 25 | async handler(argv) { 26 | try { 27 | const response = await metabase_async_await.update(argv.originId, argv.destId, argv.databaseId) 28 | console.log("\n--------Question Updated!-------") 29 | console.log("Response status code", response.status, response.statusText) 30 | console.log("ID:", response.data.id) 31 | console.log("Name:", response.data.name) 32 | console.log("Collection:", response.data.collection.name) 33 | console.log("Updated At:", response.data.updated_at) 34 | console.log("Database ID:", response.data.dataset_query.database) 35 | } catch (error) { 36 | console.log("\nError!", error.response.status, error.response.statusText); 37 | console.log(error.response.data); 38 | } 39 | } 40 | }) 41 | 42 | yargs.command({ 43 | command: 'duplicate', 44 | describe: 'Duplicate question', 45 | builder: { 46 | questionId: { 47 | describe: 'Question that will be duplicated', 48 | demandOption: true, 49 | type: 'number' 50 | }, 51 | name: { 52 | describe: 'New question name', 53 | demandOption: false, 54 | type: 'string' 55 | }, 56 | collectionId: { 57 | describe: 'Collection of new question', 58 | demandOption: true, 59 | type: 'number' 60 | }, 61 | databaseId: { 62 | describe: 'Database ID for new question', 63 | demandOption: true, 64 | type: 'number' 65 | } 66 | }, 67 | async handler(argv) { 68 | try { 69 | const response = await metabase_async_await.duplicate(argv.questionId, argv.collectionId, argv.name, argv.databaseId) 70 | console.log("\n--------New Question Created!-------") 71 | console.log("Response status code", response.status, response.statusText) 72 | console.log("ID:", response.data.id) 73 | console.log("Name:", response.data.name) 74 | console.log("Collection:", response.data.collection.name) 75 | console.log("Updated At:", response.data.updated_at) 76 | console.log("Database ID:", response.data.dataset_query.database) 77 | } catch (error) { 78 | if (error.response) { 79 | console.log("\nError!", error.response.status, error.response.statusText); 80 | console.log(error.response.data); 81 | } else{ 82 | console.log(error); 83 | } 84 | 85 | } 86 | } 87 | }) 88 | 89 | yargs.command({ 90 | command: 'duplicateAcross', 91 | describe: 'Duplicate question across environments', 92 | builder: { 93 | questionId: { 94 | describe: 'Question that will be duplicated on destination', 95 | demandOption: true, 96 | type: 'number' 97 | }, 98 | name: { 99 | describe: 'New question name', 100 | demandOption: false, 101 | type: 'string' 102 | }, 103 | collectionId: { 104 | describe: 'Destination collection of new question', 105 | demandOption: true, 106 | type: 'number' 107 | }, 108 | databaseId: { 109 | describe: 'Destination Database ID for new question', 110 | demandOption: true, 111 | type: 'number' 112 | } 113 | }, 114 | async handler(argv) { 115 | try { 116 | const response = await metabase_async_await.duplicateAcross(argv.questionId, argv.collectionId, argv.name, argv.databaseId) 117 | console.log("\n--------New Question Created!-------") 118 | console.log("Response status code", response.status, response.statusText) 119 | console.log("ID:", response.data.id) 120 | console.log("Name:", response.data.name) 121 | console.log("Collection:", response.data.collection.name) 122 | console.log("Updated At:", response.data.updated_at) 123 | console.log("Database ID:", response.data.dataset_query.database) 124 | } catch (error) { 125 | if (error.response) { 126 | console.log("\nError!", error.response.status, error.response.statusText); 127 | console.log(error.response.data); 128 | } else{ 129 | console.log(error); 130 | } 131 | 132 | } 133 | } 134 | }) 135 | 136 | yargs.command({ 137 | command: 'update-deprecated', 138 | describe: 'Update question', 139 | builder: { 140 | origin: { 141 | describe: 'Origin question', 142 | demandOption: true, 143 | type: 'number' 144 | }, 145 | dest: { 146 | describe: 'Destination question', 147 | demandOption: true, 148 | type: 'number' 149 | }, 150 | databaseId: { 151 | describe: 'Database ID for destination question', 152 | demandOption: true, 153 | type: 'number' 154 | } 155 | }, 156 | handler(argv) { 157 | metabase.update(argv.origin, argv.dest, argv.databaseId, (error, response)=>{ 158 | if (error){ 159 | console.log(error) 160 | return 161 | } 162 | 163 | if (response.body.errors){ 164 | console.log(response.body.errors) 165 | return 166 | } 167 | 168 | console.log("Response status code", response.statusCode) 169 | console.log("Question updated!") 170 | console.log("ID:", response.body.id) 171 | console.log("Name:", response.body.name) 172 | console.log("Collection:", response.body.collection.name) 173 | console.log("Updated At:", response.body.updated_at) 174 | console.log("Database ID:", response.body.dataset_query.database) 175 | }) 176 | } 177 | }) 178 | 179 | yargs.command({ 180 | command: 'duplicate-deprecated', 181 | describe: 'Duplicate question', 182 | builder: { 183 | questionId: { 184 | describe: 'Question that will be duplicated', 185 | demandOption: true, 186 | type: 'number' 187 | }, 188 | name: { 189 | describe: 'New question name', 190 | demandOption: false, 191 | type: 'string' 192 | }, 193 | collectionId: { 194 | describe: 'Collection of new question', 195 | demandOption: true, 196 | type: 'number' 197 | }, 198 | databaseId: { 199 | describe: 'Database ID for new question', 200 | demandOption: true, 201 | type: 'number' 202 | } 203 | }, 204 | handler(argv) { 205 | metabase.duplicate(argv.questionId, argv.collectionId, argv.name, argv.databaseId, (error, response) => { 206 | if (error){ 207 | console.log(error) 208 | return 209 | } 210 | 211 | if (response.body.errors){ 212 | console.log(response.body.errors) 213 | return 214 | } 215 | 216 | console.log("Response status code:", response.statusCode) 217 | console.log("New question created!") 218 | console.log("ID:", response.body.id) 219 | console.log("Name:", response.body.name) 220 | console.log("Collection:", response.body.collection.name) 221 | console.log("Created At:", response.body.created_at) 222 | console.log("Database ID:", response.body.dataset_query.database) 223 | }) 224 | } 225 | }) 226 | 227 | 228 | 229 | yargs.parse() --------------------------------------------------------------------------------