losant-cli
Advanced tools
Comparing version 1.1.2 to 1.2.0
@@ -12,3 +12,4 @@ #!/usr/bin/env node | ||
.command('experience', 'Manages your Losant Application\'s Experience Views, and Versions from the command line.') | ||
.command('files', 'Manage Files on Losant for your Application.'); | ||
.command('files', 'Manage Files on Losant for your Application.') | ||
.command('datatables', 'Manage Data Tables on Losant for your Application.'); | ||
@@ -15,0 +16,0 @@ const results = pgm.parse(process.argv); |
@@ -10,3 +10,5 @@ const error = require('error/typed'); | ||
const params = require('../../lib/get-download-params'); | ||
const dtParams = require('../../lib/get-export-params').dataTables; | ||
const getDownloader = require('../../lib/get-downloader'); | ||
const getExporter = require('../../lib/get-exporter'); | ||
const experienceBootstrap = require('../../lib/experience-bootstrap'); | ||
@@ -16,7 +18,9 @@ const inquirer = require('inquirer'); | ||
const filesDownload = getDownloader(params.files); | ||
const dataTablesExport = getExporter(dtParams); | ||
const { | ||
saveConfig, logError, logResult, log, loadUserConfig, saveLocalMeta, hasBootstrapped | ||
saveConfig, logError, logResult, log, loadUserConfig, saveLocalMeta, hasBootstrapped, getApiURL | ||
} = require('../../lib/utils'); | ||
const DIRECTORIES_TO_GENERATE = [ | ||
'dataTables', | ||
'files', | ||
@@ -31,3 +35,3 @@ 'experience' | ||
const getApplicationFunc = (api) => { | ||
const getApplicationFunc = (api, appUrl) => { | ||
return async () => { | ||
@@ -48,3 +52,3 @@ const { filter } = await inquirer.prompt([ | ||
const choices = applications.items.map((appInfo) => { | ||
const key = `${appInfo.name} https://app.losant.com/applications/${appInfo.id}`; | ||
const key = `${appInfo.name} | ${appInfo.organizationName || 'My Sandbox'} | ${appUrl}/applications/${appInfo.id}`; | ||
nameToId[key] = appInfo; | ||
@@ -102,10 +106,9 @@ return key; | ||
.action(async (command) => { | ||
const userConfig = await loadUserConfig() || {}; | ||
if (!userConfig.apiToken) { | ||
return logError('Must run losant login before running losant configure.'); | ||
} | ||
let userConfig = await loadUserConfig() || {}; | ||
const apiUrl = await getApiURL(userConfig); | ||
await Promise.all(DIRECTORIES_TO_GENERATE.map((dir) => { return ensureDir(dir); })); | ||
await Promise.all(LOCAL_META_FILES.map((type) => { return saveLocalMeta(type, {}); })); | ||
const api = await getApi({ apiToken: userConfig.apiToken }); | ||
const getApplication = getApplicationFunc(api); | ||
userConfig = userConfig[apiUrl]; | ||
const api = await getApi({ url: apiUrl, apiToken: userConfig.apiToken }); | ||
const getApplication = getApplicationFunc(api, userConfig.appUrl); | ||
let appInfo; | ||
@@ -120,3 +123,3 @@ try { | ||
} | ||
const config = { applicationId: appInfo.id, applicationName: appInfo.name }; | ||
const config = { applicationId: appInfo.id, applicationName: appInfo.name, apiUrl }; | ||
try { | ||
@@ -129,2 +132,3 @@ const file = await saveConfig(command.config, config); | ||
const loadedConfig = merge(userConfig, config); | ||
loadedConfig.api = api; | ||
try { | ||
@@ -164,2 +168,14 @@ const downloaded = await experienceDownload(null, {}, loadedConfig); | ||
} | ||
try { | ||
const { canExportDataTables } = await inquirer.prompt([{ type: 'confirm', name: 'canExportDataTables', message: 'Export data tables now?' }]); | ||
if (canExportDataTables) { | ||
const exportedTables = await dataTablesExport(null, {}, loadedConfig); | ||
if (exportedTables) { | ||
logResult('success', 'Exported all data tables!', 'green'); | ||
} | ||
} | ||
} catch (e) { | ||
console.error(e); | ||
logError('Failed to export data tables.'); | ||
} | ||
log('Configuration completed! :D'); | ||
@@ -166,0 +182,0 @@ }); |
@@ -41,3 +41,10 @@ const error = require('error/typed'); | ||
try { | ||
const userFile = await saveUserConfig({ apiToken: api.getOption('accessToken') }); | ||
const wlInfo = await api.request({ method: 'get', url: '/whitelabels/domain' }); | ||
const userFile = await saveUserConfig({ | ||
[api.getOption('url')]: { | ||
apiToken: api.getOption('accessToken'), | ||
appUrl: wlInfo.appUrl, | ||
endpointDomain: wlInfo.endpointDomain | ||
} | ||
}); | ||
logResult('success', `configuration written to ${c.bold(userFile)} with your user token!`, 'green'); | ||
@@ -44,0 +51,0 @@ } catch (e) { |
@@ -7,3 +7,4 @@ const path = require('path'); | ||
localStatusParams: [ path.join(path.sep, '{components,layouts,pages}', '*.hbs') ], | ||
remoteStatusParams: [ path.join('experience', '${viewType}s', '${name}.hbs'), 'body' ] // eslint-disable-line no-template-curly-in-string | ||
// eslint-disable-next-line no-template-curly-in-string | ||
remoteStatusParams: [ path.join('experience', '${viewType}s', '${name}.hbs'), 'body' ] | ||
}, | ||
@@ -14,4 +15,12 @@ files: { | ||
localStatusParams: [ path.join(path.sep, '**') ], | ||
remoteStatusParams: [ 'files${parentDirectory}${name}', 's3etag', { skipMd5Creation: true } ] // eslint-disable-line no-template-curly-in-string | ||
// eslint-disable-next-line no-template-curly-in-string | ||
remoteStatusParams: [ 'files${parentDirectory}${name}', 's3etag', { skipMd5Creation: true } ] | ||
}, | ||
dataTables: { | ||
commandType: 'dataTables', | ||
apiType: 'dataTables', | ||
localStatusParams: [ path.join(path.sep, '*.csv' )], | ||
// eslint-disable-next-line no-template-curly-in-string | ||
remoteStatusParams: [ path.join('dataTables', '${name}-${id}.csv') ] | ||
}, | ||
options: { | ||
@@ -18,0 +27,0 @@ dryRun: [ '--dry-run', 'display actions but do not perform them' ], |
@@ -7,3 +7,2 @@ const { | ||
} = require('./utils'); | ||
const getApi = require('./get-api'); | ||
const { experience } = require('./get-download-params'); | ||
@@ -15,4 +14,3 @@ const getDownloader = require('./get-downloader'); | ||
module.exports = async (cmd, loadedConfig, applicationInfo) => { | ||
const { apiToken, applicationId, applicationName } = loadedConfig ? loadedConfig : await loadConfig(); | ||
const api = await getApi({ apiToken }); | ||
const { apiToken, applicationId, applicationName, api, endpointDomain } = loadedConfig || await loadConfig(); | ||
try { | ||
@@ -36,4 +34,4 @@ applicationInfo = applicationInfo || await api.application.get({ applicationId }); | ||
const results = await api.experience.bootstrap({ applicationId }); | ||
await downloader(null, {}, { apiToken, applicationId }); | ||
logResult('Experience URL', `https://${applicationInfo.endpointSlug}.onlosant.com/${results.resourceSuffix}`); | ||
await downloader(null, {}, { apiToken, applicationId, api }); | ||
logResult('Experience URL', `https://${applicationInfo.endpointSlug}.${endpointDomain}/${results.resourceSuffix}`); | ||
logResult('Bootstrap Username', `${results.email}`); | ||
@@ -40,0 +38,0 @@ logResult('Bootstrap Password', `${results.password}`); |
@@ -8,3 +8,2 @@ const { | ||
const inquirer = require('inquirer'); | ||
const getApi = require('./get-api'); | ||
const paginate = require('./paginate-request'); | ||
@@ -18,5 +17,4 @@ const printTable = require('./print-table'); | ||
module.exports = async (page, command) => { | ||
const { apiToken, applicationId } = await loadConfig(); | ||
const { apiToken, applicationId, api, appUrl } = await loadConfig(); | ||
if (!applicationId || !apiToken) { return; } // config did not lock or did not load correctly. | ||
const api = await getApi({ apiToken }); | ||
const pageQuery = { applicationId, viewType: 'page' }; | ||
@@ -49,3 +47,3 @@ if (page || command.pattern) { | ||
if (pageInfos.length === 1) { | ||
log(`Only one page found that matched ${page}, ${pageInfos[0].name} https://app.losant.com/applications/${applicationId}/experience/versions/develop/views/pages/${pageInfos[0].id}`); | ||
log(`Only one page found that matched ${page}, ${pageInfos[0].name} ${appUrl}/applications/${applicationId}/experience/versions/develop/views/pages/${pageInfos[0].id}`); | ||
} | ||
@@ -56,3 +54,3 @@ if (pageInfos.length > 1) { | ||
// intentional, cause inquirer leaves no room in between the bullet point and the name, the space is important. | ||
const key = ` ${info.name} https://app.losant.com/applications/${applicationId}/experience/versions/develop/views/pages/${info.id}`; | ||
const key = ` ${info.name} ${appUrl}/applications/${applicationId}/experience/versions/develop/views/pages/${info.id}`; | ||
pageToInfo[trim(key)] = info; | ||
@@ -77,3 +75,3 @@ return key; | ||
const choices = layouts.map(({ id, name }) => { | ||
const key = `${name} https://app.losant.com/applications/${applicationId}/experience/versions/develop/views/layouts/${id}`; | ||
const key = `${name} ${appUrl}/applications/${applicationId}/experience/versions/develop/views/layouts/${id}`; | ||
keyToId[key] = id; | ||
@@ -80,0 +78,0 @@ return key; |
@@ -1,2 +0,1 @@ | ||
const getApi = require('./get-api'); | ||
const inquirer = require('inquirer'); | ||
@@ -14,3 +13,3 @@ const { | ||
const listVersions = async (api, applicationId, filter) => { | ||
const listVersions = async (api, applicationId, filter, endpointDomain) => { | ||
const query = { applicationId }; | ||
@@ -33,3 +32,3 @@ if (filter) { query.filter = filter; } | ||
if (config.slug) { | ||
return `${config.slug}.onlosant.com`; | ||
return `${config.slug}.${endpointDomain}`; | ||
} | ||
@@ -80,7 +79,6 @@ })).join('\n') : ''; | ||
module.exports = async (version, command) => { | ||
const { apiToken, applicationId } = await loadConfig(); | ||
const { apiToken, applicationId, api, endpointDomain } = await loadConfig(); | ||
if (!applicationId || !apiToken) { return; } // config did not lock or did not load correctly. | ||
const api = await getApi({ apiToken }); | ||
if (!version) { | ||
await listVersions(api, applicationId, command.list); | ||
await listVersions(api, applicationId, command.list, endpointDomain); | ||
} else { | ||
@@ -87,0 +85,0 @@ const domainChoiceMappings = choices('domainName', 'version', await getExperiencePart(api, 'domain', { applicationId })); |
const losant = require('losant-rest'); | ||
const { trim } = require('omnibelt'); | ||
process.env.LOSANT_API_URL = process.env.LOSANT_API_URL || 'https://api.losant.com'; | ||
module.exports = async ({ email, password, twoFactorCode, apiToken }) => { | ||
const api = losant.createClient({ url: process.env.LOSANT_API_URL }); | ||
module.exports = async ({ url, email, password, twoFactorCode, apiToken } = {}) => { | ||
const api = losant.createClient({ url: url || process.env.LOSANT_API_URL || 'https://api.losant.com' }); | ||
if (email) { | ||
@@ -9,0 +7,0 @@ // not try catching this, should only be used on configure and the errors will be handled there. |
const request = require('request-promise'); | ||
const { experience, files } = require('./constants'); | ||
const { endsWith } = require('omnibelt'); | ||
@@ -15,4 +16,4 @@ const experienceParams = { | ||
const filesParams = { | ||
getData: async (file) => { | ||
return request({ method: 'GET', uri: file.url, encoding: null, strictSSL: (process.env.LOSANT_API_URL !== 'https://api.losant.space') }); | ||
getData: async (file, api) => { | ||
return request({ method: 'GET', uri: file.url, encoding: null, strictSSL: !endsWith('space', api.getOption('url')) }); | ||
}, | ||
@@ -19,0 +20,0 @@ apiType: files.apiType, |
@@ -1,2 +0,1 @@ | ||
const getApi = require('./get-api'); | ||
const paginateRequest = require('./paginate-request'); | ||
@@ -34,6 +33,5 @@ const path = require('path'); | ||
let didDownload = false; | ||
const { apiToken, applicationId } = loadedConfig ? loadedConfig : await loadConfig(); | ||
const { apiToken, applicationId, api } = loadedConfig ? loadedConfig : await loadConfig(); | ||
if (!apiToken || !applicationId) { return; } | ||
if (commandType === 'experience' && !isValidExperienceOptions(command)) { return; } | ||
const api = await getApi({ apiToken }); | ||
const meta = await loadLocalMeta(commandType) || {}; | ||
@@ -129,3 +127,3 @@ let items; | ||
try { | ||
data = await getData(itemsById[remoteStatus.id]); | ||
data = await getData(itemsById[remoteStatus.id], api); | ||
} catch (e) { | ||
@@ -132,0 +130,0 @@ return logError(`An Error occurred when trying to download data for file ${remoteStatus.file} with the message ${e.message}`); |
const path = require('path'); | ||
const getApi = require('./get-api'); | ||
const paginateRequest = require('./paginate-request'); | ||
@@ -52,5 +51,4 @@ const { | ||
const getStatus = async () => { | ||
const { applicationId, apiToken } = await loadConfig(); | ||
const { applicationId, apiToken, api } = await loadConfig(); | ||
if (!applicationId || !apiToken) { return; } | ||
const api = await getApi({ apiToken }); | ||
let items; | ||
@@ -57,0 +55,0 @@ try { |
@@ -1,2 +0,1 @@ | ||
const getApi = require('./get-api'); | ||
const paginateRequest = require('./paginate-request'); | ||
@@ -27,3 +26,3 @@ const { | ||
if (isEmpty(config)) { return; } | ||
const api = await getApi({ apiToken: config.apiToken }); | ||
const api = config.api; | ||
const meta = await loadLocalMeta(commandType) || {}; | ||
@@ -30,0 +29,0 @@ let items; |
@@ -1,14 +0,19 @@ | ||
const { isNil, merge } = require('omnibelt'); | ||
module.exports = async (request, query) => { | ||
const { merge } = require('omnibelt'); | ||
module.exports = async (request, query, shouldUseOffset = false) => { | ||
let items = []; | ||
if (!query.perPage) { query.perPage = 1000; } | ||
if (isNil(query.page)) { query.page = 0; } | ||
const results = await request(query) || { items: [] }; | ||
let extra = {}; | ||
const perPage = 1000; | ||
let page = 0; | ||
extra = (shouldUseOffset && { offset: page*perPage, limit: perPage })|| { page, perPage }; | ||
const results = await request(merge(query, extra)) || { items: [] }; | ||
if (results.count) { items = results.items; } | ||
if (results.totalCount > 1000) { | ||
const totalPages = Math.ceil(results.totalCount / 1000); | ||
let page = query.page + 1; // since you already made one request above | ||
const totalPages = Math.ceil(results.totalCount / perPage); | ||
++page; // since you already made one request above | ||
const requests = []; | ||
while (page <= totalPages) { | ||
requests.push(request(merge(query), { page })); | ||
extra = (shouldUseOffset && { offset: page*perPage, limit: perPage })|| { page, perPage }; | ||
requests.push(request(merge(query, extra))); | ||
++page; | ||
@@ -15,0 +20,0 @@ } |
141
lib/utils.js
@@ -0,1 +1,2 @@ | ||
const getApi = require('./get-api'); | ||
const path = require('path'); | ||
@@ -12,3 +13,3 @@ const findFile = require('find-file-up'); | ||
const { | ||
keyBy, prop, merge, isEmpty, keys, union, find, propEq, propOr | ||
keyBy, prop, mergeRight, isEmpty, keys, union, find, propEq, propOr, mapP | ||
} = require('omnibelt'); | ||
@@ -21,3 +22,3 @@ const { rollbarLog } = require('./rollbar'); | ||
const resolveUserConfig = () => { | ||
const directory = process.env.NODE_ENV !== 'test' ? (process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE) : './'; | ||
const directory = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE; | ||
return path.resolve(directory, path.join(`${CONFIG_DIR}`, '.credentials.yml')); | ||
@@ -32,2 +33,4 @@ }; | ||
} = require('fs-extra'); | ||
const jwt = require('jsonwebtoken'); | ||
const inquirer = require('inquirer'); | ||
@@ -70,7 +73,34 @@ const utils = { | ||
loadUserConfig: async () => { | ||
loadUserConfig: async (checkIsEmpty = true) => { | ||
const f = resolveUserConfig(); | ||
await ensureFile(f); | ||
const config = yaml.safeLoad(await readFile(f)) || {}; | ||
if (isEmpty(config)) { | ||
let config = yaml.safeLoad(await readFile(f)) || {}; | ||
if (config.apiToken) { | ||
const token = config.apiToken; | ||
config = {}; | ||
const decoded = jwt.decode(token, { complete: true }) || {}; | ||
if (decoded.payload && decoded.payload.iss) { | ||
if (decoded.payload.iss === 'api.getstructure.io') { | ||
decoded.payload.iss = 'api.losant.com'; | ||
} | ||
const defaultUrl = `https://${decoded.payload.iss}`; | ||
const api = await getApi({ apiToken: token, url: defaultUrl }); | ||
let wlInfo; | ||
try { | ||
wlInfo = await api.request({ method: 'get', url: '/whitelabels/domain' }); | ||
} catch (e) { | ||
// could not get the white-label... | ||
// just throw away the token they need to re login. | ||
} | ||
if (wlInfo) { | ||
config[defaultUrl] = { | ||
apiToken: token, | ||
endpointDomain: wlInfo.endpointDomain, | ||
appUrl: wlInfo.appUrl | ||
}; | ||
await writeFile(f, yaml.safeDump(config)); | ||
} | ||
} | ||
} | ||
if (checkIsEmpty && isEmpty(config)) { | ||
utils.logError('User Configuration file missing, run losant login to generate this file.'); | ||
@@ -82,3 +112,17 @@ process.exit(1); | ||
loadApplicationConfig: async (file) => { | ||
getApiURL: async (userConfig) => { | ||
const apiKeys = keys(userConfig); | ||
if (apiKeys.length === 1) { | ||
return apiKeys[0]; | ||
} | ||
const { url } = await inquirer.prompt([{ | ||
type: 'list', | ||
name: 'url', | ||
message: 'Choose an API url:', | ||
choices: apiKeys | ||
}]); | ||
return url; | ||
}, | ||
loadApplicationConfig: async (file, userConfig) => { | ||
const f = configName(file); | ||
@@ -103,3 +147,7 @@ let foundFile = resolveConfig(file); | ||
} | ||
config.file = file; | ||
if (!config.apiUrl) { | ||
config.apiUrl = await utils.getApiURL(userConfig); | ||
await writeFile(foundFile, yaml.safeDump(config)); | ||
} | ||
config.file = foundFile; | ||
} | ||
@@ -110,5 +158,20 @@ return config; | ||
loadConfig: async (file) => { | ||
const userConfig = await utils.loadUserConfig(file); | ||
const appConfig = await utils.loadApplicationConfig(file); | ||
return merge(userConfig, appConfig); | ||
let userConfig = await utils.loadUserConfig(); | ||
const appConfig = await utils.loadApplicationConfig(file, userConfig); | ||
// this is handled by all other commands to return early if this function returns an empty object | ||
// the commands assume something was logged | ||
// originally this path was taken to make testing easier. | ||
// TODO UPDATE how errors are logged and either rollbar and exit or continue if it's tests running. | ||
if (!appConfig) { return {}; } | ||
// backwards compatible in case the current applicationConfig does not have an apiUrl | ||
const apiKey = appConfig.apiUrl; | ||
userConfig = userConfig[apiKey]; | ||
if (!userConfig || isEmpty(userConfig)) { | ||
utils.logError(`Could not find log in information for API ${apiKey}, please login again.`); | ||
process.exit(1); | ||
} | ||
const api = await getApi({ url: apiKey, apiToken: userConfig.apiToken }); | ||
const config = mergeRight(userConfig, appConfig); | ||
config.api = api; | ||
return config; | ||
}, | ||
@@ -157,2 +220,4 @@ | ||
saveUserConfig: async (config) => { | ||
const userConfig = await utils.loadUserConfig(false); | ||
config = mergeRight(userConfig, config); | ||
const conf = yaml.safeDump(config); | ||
@@ -199,2 +264,10 @@ const f = resolveUserConfig(); | ||
getShallowLocalStatus: async (dir, globPattern) => { | ||
const dirPattern = path.resolve(path.join(dir, globPattern)); | ||
const files = glob.sync(dirPattern); | ||
return new Map(await mapP(async (file) => { | ||
return [path.relative('.', file), true]; | ||
}, files)); | ||
}, | ||
getLocalStatus: async (type, dir, globPattern) => { | ||
@@ -204,3 +277,3 @@ if (!dir) { dir = type; } // the default pattern is that the directory matches the command type. e.g. files files/ | ||
const meta = await utils.loadLocalMeta(type) || {}; | ||
const metaFiles = new Set(Object.keys(meta)); | ||
const metaFiles = new Set(keys(meta)); | ||
const dirPattern = path.resolve(path.join(dir, globPattern)); | ||
@@ -248,2 +321,27 @@ const files = glob.sync(dirPattern); | ||
getShallowRemoteStatus: async (localFiles, resources, pathTemplate) => { | ||
if (!resources) { return {}; } | ||
const statusByFile = {}; | ||
const compiledTemplate = template(pathTemplate); | ||
resources.forEach((resource) => { | ||
const file = path.normalize(compiledTemplate(resource)); | ||
const statObj = { | ||
id: resource.id, | ||
file, | ||
name: resource.name, | ||
status: 'found' | ||
}; | ||
statusByFile[file] = statObj; | ||
localFiles.delete(file); | ||
}); | ||
const files = Array.from(localFiles.keys()); | ||
files.forEach((file) => { | ||
statusByFile[file] = { | ||
file, | ||
status: 'missing' | ||
}; | ||
}); | ||
return statusByFile; | ||
}, | ||
getRemoteStatus: async (type, resources, pathTemplate, contentProperty, options = {}) => { | ||
@@ -254,3 +352,3 @@ const statusByFile = {}; | ||
const metaFiles = new Set(); | ||
Object.keys(meta).forEach((file) => { | ||
keys(meta).forEach((file) => { | ||
const item = meta[file]; | ||
@@ -336,2 +434,21 @@ metaByFile[file] = Object.assign({}, item, { file: file }); | ||
getShallowStatus: async ({ items, remoteStatusParams, localStatusParams, dir, pattern }) => { | ||
const localFiles = await utils.getShallowLocalStatus(dir, ...localStatusParams); | ||
const remoteStatusByFile = await utils.getShallowRemoteStatus(new Map(localFiles), items, ...remoteStatusParams); | ||
if (pattern) { | ||
const dirPattern = path.join(dir, pattern); | ||
const allFiles = union(keys(remoteStatusByFile), keys(localFiles)); | ||
const matchPattern = new RegExp(`^${dirPattern}.*$`, 'i'); | ||
allFiles.forEach((file) => { | ||
if (!matchPattern.test(file)) { | ||
delete remoteStatusByFile[file]; | ||
delete localFiles[file]; | ||
} | ||
}); | ||
} | ||
return { localStatusByFile: localFiles, remoteStatusByFile }; | ||
}, | ||
getStatus: async ({ | ||
@@ -338,0 +455,0 @@ commandType, items, remoteStatusParams, localStatusParams, pattern, type, dir |
{ | ||
"name": "losant-cli", | ||
"version": "1.1.2", | ||
"version": "1.2.0", | ||
"description": "Losant Command Line Interface", | ||
@@ -40,2 +40,3 @@ "license": "MIT", | ||
"commander": "^2.19.0", | ||
"csv-stringify": "^5.3.0", | ||
"death": "^1.1.0", | ||
@@ -49,8 +50,9 @@ "error": "^7.0.2", | ||
"js-yaml": "^3.12.0", | ||
"jsonwebtoken": "^8.5.1", | ||
"lodash-template": "^1.0.0", | ||
"losant-rest": "2.3.5", | ||
"losant-rest": "2.3.7", | ||
"mime-types": "^2.1.21", | ||
"minimatch": "^3.0.4", | ||
"moment": "^2.22.2", | ||
"omnibelt": "^1.2.0", | ||
"omnibelt": "^1.3.1", | ||
"pad": "^2.2.1", | ||
@@ -61,2 +63,3 @@ "proper-lockfile": "^3.0.2", | ||
"rollbar": "^2.5.0", | ||
"sanitize-filename": "^1.6.2", | ||
"single-line-log": "^1.1.2", | ||
@@ -63,0 +66,0 @@ "update-notifier": "^2.3.0" |
@@ -8,3 +8,3 @@ # Losant CLI | ||
[Losant CLI](https://docs.losant.com/cli/overview) is a command line tool to help manage your [Losant Application](https://docs.losant.com/applications/overview/) and its resources. | ||
It easily lets you manage [Experience Views](https://docs.losant.com/experiences/views/), [Experience Versions](https://docs.losant.com/experiences/versions/), and [Files](https://docs.losant.com/applications/files/) in your Applications. | ||
It easily lets you manage [Experience Views](https://docs.losant.com/experiences/views/), [Experience Versions](https://docs.losant.com/experiences/versions/), [Files](https://docs.losant.com/applications/files/), and [Data Tables](https://docs.losant.com/data-tables/overview/) in your Applications. | ||
@@ -31,2 +31,3 @@ ## Installation | ||
* [files](#files) | ||
* [datatables](#data-tables) | ||
@@ -125,2 +126,17 @@ ### Login | ||
### Data Tables | ||
The `losant datatables` command is how you manage the data tables for a configured Application. It has the following subcommands: | ||
* export | ||
#### Data Tables Examples | ||
* Export all data tables | ||
`$ losant datatables export` | ||
* Export all data tables whose names start with `Chicago` | ||
`$ losant datatables export Chicago` | ||
* Force a export of all data tables overwriting local modifications | ||
`$ losant datatables export -f` | ||
***** | ||
@@ -130,2 +146,2 @@ | ||
<https://www.losant.com> | ||
<https://www.losant.com> |
86725
54
2112
144
15
29
+ Addedcsv-stringify@^5.3.0
+ Addedjsonwebtoken@^8.5.1
+ Addedsanitize-filename@^1.6.2
+ Addedbuffer-equal-constant-time@1.0.1(transitive)
+ Addedcsv-stringify@5.6.5(transitive)
+ Addedecdsa-sig-formatter@1.0.11(transitive)
+ Addedjsonwebtoken@8.5.1(transitive)
+ Addedjwa@1.4.1(transitive)
+ Addedjws@3.2.2(transitive)
+ Addedlodash.includes@4.3.0(transitive)
+ Addedlodash.isboolean@3.0.3(transitive)
+ Addedlodash.isinteger@4.0.4(transitive)
+ Addedlodash.isnumber@3.0.3(transitive)
+ Addedlodash.isplainobject@4.0.6(transitive)
+ Addedlodash.isstring@4.0.1(transitive)
+ Addedlodash.once@4.1.1(transitive)
+ Addedlosant-rest@2.3.7(transitive)
+ Addedms@2.1.3(transitive)
+ Addedsanitize-filename@1.6.3(transitive)
+ Addedtruncate-utf8-bytes@1.0.2(transitive)
+ Addedutf8-byte-length@1.0.5(transitive)
- Removedlosant-rest@2.3.5(transitive)
Updatedlosant-rest@2.3.7
Updatedomnibelt@^1.3.1