From 8e254065d8c9812cfac177812ed98d6faba27e2a Mon Sep 17 00:00:00 2001 From: Bjoern Date: Thu, 17 Dec 2020 17:55:37 +0000 Subject: [PATCH 01/10] Update zotero-cli.ts Split into cli and api-lib-ts --- bin/zotero-cli.ts | 863 ---------------------------------------------- 1 file changed, 863 deletions(-) diff --git a/bin/zotero-cli.ts b/bin/zotero-cli.ts index 0fb4160..3335a4e 100755 --- a/bin/zotero-cli.ts +++ b/bin/zotero-cli.ts @@ -1,867 +1,4 @@ -#!/usr/bin/env node -require('dotenv').config() -require('docstring') -const os = require('os') - -import { ArgumentParser } from 'argparse' -import { parse as TOML } from '@iarna/toml' -import fs = require('fs') -import path = require('path') - -import request = require('request-promise') -import * as LinkHeader from 'http-link-header' - -import Ajv = require('ajv') -const ajv = new Ajv() - -const md5 = require('md5-file') - -function sleep(msecs) { - return new Promise(resolve => setTimeout(resolve, msecs)) -} - -const arg = new class { - integer(v) { - if (isNaN(parseInt(v))) throw new Error(`${JSON.stringify(v)} is not an integer`) - return parseInt(v) - } - - file(v) { - if (!fs.existsSync(v) || !fs.lstatSync(v).isFile()) throw new Error(`${JSON.stringify(v)} is not a file`) - return v - } - - path(v) { - if (!fs.existsSync(v)) throw new Error(`${JSON.stringify(v)} does not exist`) - return v - } - - json(v) { - return JSON.parse(v) - } -} - -class Zotero { - args: any - output: string = '' - parser: any - config: any - zotero: any - base = 'https://api.zotero.org' - headers = { - 'User-Agent': 'Zotero-CLI', - 'Zotero-API-Version': '3', - } - - async run() { - this.output = '' - // global parameters for all commands - this.parser = new ArgumentParser - this.parser.addArgument('--api-key', { help: 'The API key to access the Zotero API.' }) - this.parser.addArgument('--config', { type: arg.file, help: 'Configuration file (toml format). Note that ./zotero-cli.toml and ~/.config/zotero-cli/zotero-cli.toml is picked up automatically.' }) - this.parser.addArgument('--user-id', { type: arg.integer, help: 'The id of the user library.' }) - this.parser.addArgument('--group-id', { type: arg.integer, help: 'The id of the group library.' }) - // See below. If changed, add: You can provide the group-id as zotero-select link (zotero://...). Only the group-id is used, the item/collection id is discarded. - this.parser.addArgument('--indent', { type: arg.integer, help: 'Identation for json output.' }) - this.parser.addArgument('--out', { help: 'Output to file' }) - this.parser.addArgument('--verbose', { action: 'storeTrue', help: 'Log requests.' }) - - const subparsers = this.parser.addSubparsers({ title: 'commands', dest: 'command', required: true }) - // add all methods that do not start with _ as a command - for (const cmd of Object.getOwnPropertyNames(Object.getPrototypeOf(this)).sort()) { - if (typeof this[cmd] !== 'function' || cmd[0] !== '$') continue - - const sp = subparsers.addParser(cmd.slice(1).replace(/_/g, '-'), { description: this[cmd].__doc__, help: this[cmd].__doc__ }) - // when called with an argparser, the command is expected to add relevant parameters and return - // the command must have a docstring - this[cmd](sp) - } - - this.args = this.parser.parseArgs() - - // pick up config - const config: string = [this.args.config, 'zotero-cli.toml', `${os.homedir()}/.config/zotero-cli/zotero-cli.toml`].find(cfg => fs.existsSync(cfg)) - this.config = config ? TOML(fs.readFileSync(config, 'utf-8')) : {} - - if (this.args.user_id || this.args.group_id) { - //Overwriting command line option in config - delete this.config['user-id'] - delete this.config['group-id'] - - this.config['user-id'] = this.args.user_id - this.config['group-id'] = this.args.group_id - - if (!this.config['user-id']) delete this.config['user-id'] - if (!this.config['group-id']) delete this.config['group-id'] - } - - // expand selected command - const options = [].concat.apply([], this.parser._actions.map(action => action.dest === 'command' ? action.choices[this.args.command] : [action])) - for (const option of options) { - if (!option.dest) continue - if (['help', 'config'].includes(option.dest)) continue - - if (this.args[option.dest] !== null) continue - - let value - - // first try explicit config - if (typeof value === 'undefined' && this.args.config) { - value = (this.config[this.args.command] || {})[option.dest.replace(/_/g, '-')] - if (typeof value === 'undefined') value = this.config[option.dest.replace(/_/g, '-')] - } - - // next, ENV vars. Also picks up from .env - if (typeof value === 'undefined') { - value = process.env[`ZOTERO_CLI_${option.dest.toUpperCase()}`] || process.env[`ZOTERO_${option.dest.toUpperCase()}`] - } - - // last, implicit config - if (typeof value === 'undefined') { - value = (this.config[this.args.command] || {})[option.dest.replace(/_/g, '-')] - if (typeof value === 'undefined') value = this.config[option.dest.replace(/_/g, '-')] - } - - if (typeof value === 'undefined') continue - - if (option.type === arg.integer) { - if (isNaN(parseInt(value))) this.parser.error(`${option.dest} must be numeric, not ${value}`) - value = parseInt(value) - - } else if (option.type === arg.path) { - if (!fs.existsSync(value)) this.parser.error(`${option.dest}: ${value} does not exist`) - - } else if (option.type === arg.file) { - if (!fs.existsSync(value) || !fs.lstatSync(value).isFile()) this.parser.error(`${option.dest}: ${value} is not a file`) - - } else if (option.type === arg.json && typeof value === 'string') { - try { - value = JSON.parse(value) - } catch (err) { - this.parser.error(`${option.dest}: ${JSON.stringify(value)} is not valid JSON`) - } - - } else if (option.choices) { - if (!option.choices.includes(value)) this.parser.error(`${option.dest} must be one of ${option.choices}`) - - } else if (option.action === 'storeTrue' && typeof value === 'string') { - const _value = { - true: true, - yes: true, - on: true, - - false: false, - no: false, - off: false, - }[value] - if (typeof _value === 'undefined') this.parser.error(`%{option.dest} must be boolean, not ${value}`) - value = _value - - } else { - // string - } - - this.args[option.dest] = value - } - - if (!this.args.api_key) this.parser.error('no API key provided') - this.headers['Zotero-API-Key'] = this.args.api_key - - if (this.args.user_id === null && this.args.group_id === null) this.parser.error('You must provide exactly one of --user-id or --group-id') - if (this.args.user_id !== null && this.args.group_id !== null) this.parser.error('You must provide exactly one of --user-id or --group-id') - if (this.args.user_id === 0) this.args.user_id = (await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })).userID - - - /* - // Could do this here: - if (this.args.group_id) { - this.args.group_id = this.extractGroup(this.args.group_id) - if (!this.args.group_id) { - this.parser.error('Unable to extract group_id from the string provided via --group_id.') - return - } - } - */ - - // using default=2 above prevents the overrides from being picked up - if (this.args.indent === null) this.args.indent = 2 - - // call the actual command - try { - await this['$' + this.args.command.replace(/-/g, '_')]() - } catch (ex) { - this.print('Command execution failed: ', ex) - process.exit(1) - } - - if (this.args.out) fs.writeFileSync(this.args.out, this.output) - } - - public print(...args: any[]) { - if (!this.args.out) { - console.log.apply(console, args) - - } else { - this.output += args.map(m => { - const type = typeof m - - if (type === 'string' || m instanceof String || type === 'number' || type === 'undefined' || type === 'boolean' || m === null) return m - - if (m instanceof Error) return `` - - if (m && type === 'object' && m.message) return `` - - return JSON.stringify(m, null, this.args.indent) - - }).join(' ') + '\n' - } - } - - async all(uri, params = {}) { - let chunk = await this.get(uri, { resolveWithFullResponse: true, params }) - let data = chunk.body - - let link = chunk.headers.link && LinkHeader.parse(chunk.headers.link).rel('next') - while (link && link.length && link[0].uri) { - if (chunk.headers.backoff) await sleep(parseInt(chunk.headers.backoff) * 1000) - - chunk = await request({ - uri: link[0].uri, - headers: this.headers, - json: true, - resolveWithFullResponse: true, - }) - data = data.concat(chunk.body) - link = chunk.headers.link && LinkHeader.parse(chunk.headers.link).rel('next') - } - return data - } - - // The Zotero API uses several commands: get, post, patch, delete - these are defined below. - - async get(uri, options: { userOrGroupPrefix?: boolean, params?: any, resolveWithFullResponse?: boolean, json?: boolean } = {}) { - if (typeof options.userOrGroupPrefix === 'undefined') options.userOrGroupPrefix = true - if (typeof options.params === 'undefined') options.params = {} - if (typeof options.json === 'undefined') options.json = true - - let prefix = '' - if (options.userOrGroupPrefix) prefix = this.args.user_id ? `/users/${this.args.user_id}` : `/groups/${this.args.group_id}` - - const params = Object.keys(options.params).map(param => { - let values = options.params[param] - if (!Array.isArray(values)) values = [values] - return values.map(v => `${param}=${encodeURI(v)}`).join('&') - }).join('&') - - uri = `${this.base}${prefix}${uri}${params ? '?' + params : ''}` - if (this.args.verbose) console.error('GET', uri) - - return request({ - uri, - headers: this.headers, - encoding: null, - json: options.json, - resolveWithFullResponse: options.resolveWithFullResponse, - }) - } - - async post(uri, data, headers = {}) { - const prefix = this.args.user_id ? `/users/${this.args.user_id}` : `/groups/${this.args.group_id}` - - uri = `${this.base}${prefix}${uri}` - if (this.args.verbose) console.error('POST', uri) - - return request({ - method: 'POST', - uri, - headers: { ...this.headers, 'Content-Type': 'application/json', ...headers }, - body: data, - }) - } - - async put(uri, data) { - const prefix = this.args.user_id ? `/users/${this.args.user_id}` : `/groups/${this.args.group_id}` - - uri = `${this.base}${prefix}${uri}` - if (this.args.verbose) console.error('PUT', uri) - - return request({ - method: 'PUT', - uri, - headers: { ...this.headers, 'Content-Type': 'application/json' }, - body: data, - }) - } - - async patch(uri, data, version?: number) { - const prefix = this.args.user_id ? `/users/${this.args.user_id}` : `/groups/${this.args.group_id}` - - const headers = { ...this.headers, 'Content-Type': 'application/json' } - if (typeof version !== 'undefined') headers['If-Unmodified-Since-Version'] = version - - uri = `${this.base}${prefix}${uri}` - if (this.args.verbose) console.error('PATCH', uri) - - return request({ - method: 'PATCH', - uri, - headers, - body: data, - }) - } - - async delete(uri, version?: number) { - const prefix = this.args.user_id ? `/users/${this.args.user_id}` : `/groups/${this.args.group_id}` - - const headers = { ...this.headers, 'Content-Type': 'application/json' } - if (typeof version !== 'undefined') headers['If-Unmodified-Since-Version'] = version - - uri = `${this.base}${prefix}${uri}` - if (this.args.verbose) console.error('DELETE', uri) - - return request({ - method: 'DELETE', - uri, - headers, - }) - } - - async count(uri, params = {}) { - return (await this.get(uri, { resolveWithFullResponse: true, params })).headers['total-results'] - } - - show(v) { - this.print(JSON.stringify(v, null, this.args.indent).replace(new RegExp(this.args.api_key, 'g'), '')) - } - - extractKeyAndSetGroup(key) { - // zotero://select/groups/(\d+)/(items|collections)/([A-Z01-9]+) - var out = key; - var res = key.match(/^zotero\:\/\/select\/groups\/(library|\d+)\/(items|collections)\/([A-Z01-9]+)/) - if (res) { - if (res[2] == "library") { - console.log('You cannot specify zotero-select links (zotero://...) to select user libraries.') - return - } else { - // console.log("Key: zotero://-key provided for "+res[2]+" Setting group-id.") - this.args.group_id = res[1] - out = res[3] - }; - } - return out - } - - /// THE COMMANDS /// - // The following functions define key API commands: /keys, /collection, /collections, etc. - - // https://www.zotero.org/support/dev/web_api/v3/basics - // Collections - // /collections Collections in the library - // /collections/top Top-level collections in the library - // /collections/ A specific collection in the library - // /collections//collections Subcollections within a specific collection in the library - - // TODO: --create-child should go into 'collection'. - - async $collections(argparser = null) { - /** Retrieve a list of collections or create a collection. (API: /collections, /collections/top, /collections//collections). Use 'collections --help' for details. */ - - if (argparser) { - argparser.addArgument('--top', { action: 'storeTrue', help: 'Show only collection at top level.' }) - argparser.addArgument('--key', { help: 'Show all the child collections of collection with key. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) - argparser.addArgument('--create-child', { nargs: '*', help: 'Create child collections of key (or at the top level if no key is specified) with the names specified.' }) - return - } - - if (this.args.key) { - this.args.key = this.extractKeyAndSetGroup(this.args.key) - if (!this.args.key) { - this.parser.error('Unable to extract group/key from the string provided.') - return - } - } - - if (this.args.create_child) { - const response = await this.post('/collections', - JSON.stringify(this.args.create_child.map(c => { return { name: c, parentCollection: this.args.key } }))) - this.print('Collections created: ', JSON.parse(response).successful) - return - } - - - let collections = null; - if (this.args.key) { - collections = await this.all(`/collections/${this.args.key}/collections`) - } else { - collections = await this.all(`/collections${this.args.top ? '/top' : ''}`) - } - - this.show(collections) - } - - // Operate on a specific collection. - // /collections//items Items within a specific collection in the library - // /collections//items/top Top-level items within a specific collection in the library - - // TODO: --create-child should go into 'collection'. - // DONE: Why is does the setup for --add and --remove differ? Should 'add' not be "nargs: '*'"? Remove 'itemkeys'? - // TODO: Add option "--output file.json" to pipe output to file. - - async $collection(argparser = null) { - /** - Retrieve information about a specific collection --key KEY (API: /collections/KEY or /collections/KEY/tags). Use 'collection --help' for details. - (Note: Retrieve items is a collection via 'items --collection KEY'.) - */ - - if (argparser) { - argparser.addArgument('--key', { required: true, help: 'The key of the collection (required). You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) - argparser.addArgument('--tags', { action: 'storeTrue', help: 'Display tags present in the collection.' }) - // argparser.addArgument('itemkeys', { nargs: '*' , help: 'Item keys for items to be added or removed from this collection.'}) - argparser.addArgument('--add', { nargs: '*', help: 'Add items to this collection. Note that adding items to collections with \'item --addtocollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) - argparser.addArgument('--remove', { nargs: '*', help: 'Convenience method: Remove items from this collection. Note that removing items from collections with \'item --removefromcollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) - return - } - - if (this.args.key) { - this.args.key = this.extractKeyAndSetGroup(this.args.key) - if (!this.args.key) { - this.parser.error('Unable to extract group/key from the string provided.') - return - } - } - - if (this.args.tags && this.args.add) { - this.parser.error('--tags cannot be combined with --add') - return - } - if (this.args.tags && this.args.remove) { - this.parser.error('--tags cannot be combined with --remove') - return - } - /* - if (this.args.add && !this.args.itemkeys.length) { - this.parser.error('--add requires item keys') - return - } - if (!this.args.add && this.args.itemkeys.length) { - this.parser.error('unexpected item keys') - return - } - */ - if (this.args.add) { - for (const itemKey of this.args.add) { - const item = await this.get(`/items/${itemKey}`) - if (item.data.collections.includes(this.args.key)) continue - await this.patch(`/items/${itemKey}`, JSON.stringify({ collections: item.data.collections.concat(this.args.key) }), item.version) - } - } - - if (this.args.remove) { - for (const itemKey of this.args.remove) { - const item = await this.get(`/items/${itemKey}`) - const index = item.data.collections.indexOf(this.args.key) - if (index > -1) { - item.data.collections.splice(index, 1) - } - await this.patch(`/items/${itemKey}`, JSON.stringify({ collections: item.data.collections }), item.version) - } - } - - this.show(await this.get(`/collections/${this.args.key}${this.args.tags ? '/tags' : ''}`)) - } - - // URI Description - // https://www.zotero.org/support/dev/web_api/v3/basics - // /items All items in the library, excluding trashed items - // /items/top Top-level items in the library, excluding trashed items - - async $items(argparser = null) { - /** - Retrieve list of items from API. (API: /items, /items/top, /collections/COLLECTION/items/top). - Use 'items --help' for details. - By default, all items are retrieved. With --top or limit (via --filter) the default number of items are retrieved. - */ - - let items - - if (argparser) { - argparser.addArgument('--count', { action: 'storeTrue', help: 'Return the number of items.' }) - // argparser.addArgument('--all', { action: 'storeTrue', help: 'obsolete' }) - argparser.addArgument('--filter', { type: arg.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. For example: \'{"format": "json,bib", "limit": 100, "start": 100}\'.' }) - argparser.addArgument('--collection', { help: 'Retrive list of items for collection. You can provide the collection key as a zotero-select link (zotero://...) to also set the group-id.' }) - argparser.addArgument('--top', { action: 'storeTrue', help: 'Retrieve top-level items in the library/collection (excluding child items / attachments, excluding trashed items).' }) - argparser.addArgument('--validate', { type: arg.path, help: 'json-schema file for all itemtypes, or directory with schema files, one per itemtype.' }) - return - } - - if (this.args.count && this.args.validate) { - this.parser.error('--count cannot be combined with --validate') - return - } - - if (this.args.collection) { - this.args.collection = this.extractKeyAndSetGroup(this.args.collection) - if (!this.args.collection) { - this.parser.error('Unable to extract group/key from the string provided.') - return - } - } - - const collection = this.args.collection ? `/collections/${this.args.collection}` : '' - - if (this.args.count) { - this.print(await this.count(`${collection}/items${this.args.top ? '/top' : ''}`, this.args.filter || {})) - return - } - - const params = this.args.filter || {} - - if (this.args.top) { - // This should be all - there may be more than 100 items. - // items = await this.all(`${collection}/items/top`, { params }) - items = await this.all(`${collection}/items/top`, params ) - } else if (params.limit) { - if (params.limit > 100) { - this.parser.error('You can only retrieve up to 100 items with with params.limit.') - return - } - items = await this.get(`${collection}/items`, { params }) - } else { - items = await this.all(`${collection}/items`, params) - } - - if (this.args.validate) { - if (!fs.existsSync(this.args.validate)) throw new Error(`${this.args.validate} does not exist`) - - const oneSchema = fs.lstatSync(this.args.validate).isFile() - - let validate = oneSchema ? ajv.compile(JSON.parse(fs.readFileSync(this.args.validate, 'utf-8'))) : null - - const validators = {} - // still a bit rudimentary - for (const item of items) { - if (!oneSchema) { - validate = validators[item.itemType] = validators[item.itemType] || ajv.compile(JSON.parse(fs.readFileSync(path.join(this.args.validate, `${item.itemType}.json`), 'utf-8'))) - } - - if (!validate(item)) this.show(validate.errors) - } - - } else { - this.show(items) - } - } - - // https://www.zotero.org/support/dev/web_api/v3/basics - // /items/ A specific item in the library - // /items//children Child items under a specific item - - async $item(argparser = null) { - /** - Retrieve an item (item --key KEY), save/add file attachments, retrieve children. Manage collections and tags. (API: /items/KEY/ or /items/KEY/children). - - Also see 'attachment', 'create' and 'update'. - */ - - if (argparser) { - argparser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) - argparser.addArgument('--children', { action: 'storeTrue', help: 'Retrieve list of children for the item.' }) - argparser.addArgument('--filter', { type: arg.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. To retrieve multiple items you have use "itemkey"; for example: \'{"format": "json,bib", "itemkey": "A,B,C"}\'. See https://www.zotero.org/support/dev/web_api/v3/basics#search_syntax.' }) - argparser.addArgument('--addfile', { nargs: '*', help: 'Upload attachments to the item. (/items/new)' }) - argparser.addArgument('--savefiles', { nargs: '*', help: 'Download all attachments from the item (/items/KEY/file).' }) - argparser.addArgument('--addtocollection', { nargs: '*', help: 'Add item to collections. (Convenience method: patch item->data->collections.)' }) - argparser.addArgument('--removefromcollection', { nargs: '*', help: 'Remove item from collections. (Convenience method: patch item->data->collections.)' }) - argparser.addArgument('--addtags', { nargs: '*', help: 'Add tags to item. (Convenience method: patch item->data->tags.)' }) - argparser.addArgument('--removetags', { nargs: '*', help: 'Remove tags from item. (Convenience method: patch item->data->tags.)' }) - return - } - - - if (this.args.key) { - this.args.key = this.extractKeyAndSetGroup(this.args.key) - if (!this.args.key) { - this.parser.error('Unable to extract group/key from the string provided.') - return - } - } - - const item = await this.get(`/items/${this.args.key}`) - - if (this.args.savefiles) { - let children = await this.get(`/items/${this.args.key}/children`); - await Promise.all(children.filter(item => item.data.itemType === 'attachment').map(async item => { - console.log(`Downloading file ${item.data.filename}`) - fs.writeFileSync(item.data.filename, await this.get(`/items/${item.key}/file`), 'binary') - })) - } - - if (this.args.addfile) { - const attachmentTemplate = await this.get('/items/new?itemType=attachment&linkMode=imported_file', { userOrGroupPrefix: false }) - for (const filename of this.args.addfile) { - if (!fs.existsSync(filename)) { - console.log(`Ignoring non-existing file: ${filename}`); - return - } - - let attach = attachmentTemplate; - attach.title = path.basename(filename) - attach.filename = path.basename(filename) - attach.contentType = `application/${path.extname(filename).slice(1)}` - attach.parentItem = this.args.key - const stat = fs.statSync(filename) - const uploadItem = JSON.parse(await this.post('/items', JSON.stringify([attach]))) - const uploadAuth = JSON.parse(await this.post(`/items/${uploadItem.successful[0].key}/file?md5=${md5.sync(filename)}&filename=${attach.filename}&filesize=${fs.statSync(filename)['size']}&mtime=${stat.mtimeMs}`, '{}', { 'If-None-Match': '*' })) - if (uploadAuth.exists !== 1) { - const uploadResponse = await request({ - method: 'POST', - uri: uploadAuth.url, - body: Buffer.concat([Buffer.from(uploadAuth.prefix), fs.readFileSync(filename), Buffer.from(uploadAuth.suffix)]), - headers: { 'Content-Type': uploadAuth.contentType } - }) - await this.post(`/items/${uploadItem.successful[0].key}/file?upload=${uploadAuth.uploadKey}`, '{}', { 'Content-Type': 'application/x-www-form-urlencoded', 'If-None-Match': '*' }) - } - } - } - - if (this.args.addtocollection) { - let newCollections = item.data.collections - this.args.addtocollection.forEach(itemKey => { - if (!newCollections.includes(itemKey)) { - newCollections.push(itemKey) - } - }) - await this.patch(`/items/${this.args.key}`, JSON.stringify({ collections: newCollections }), item.version) - } - - if (this.args.removefromcollection) { - let newCollections = item.data.collections - this.args.removefromcollection.forEach(itemKey => { - const index = newCollections.indexOf(itemKey) - if (index > -1) { - newCollections.splice(index, 1) - } - }) - await this.patch(`/items/${this.args.key}`, JSON.stringify({ collections: newCollections }), item.version) - } - - if (this.args.addtags) { - let newTags = item.data.tags - this.args.addtags.forEach(tag => { - if (!newTags.find(newTag => newTag.tag === tag)) { - newTags.push({ tag }) - } - }) - await this.patch(`/items/${this.args.key}`, JSON.stringify({ tags: newTags }), item.version) - } - - if (this.args.removetags) { - let newTags = item.data.tags.filter(tag => !this.args.removetags.includes(tag.tag)) - await this.patch(`/items/${this.args.key}`, JSON.stringify({ tags: newTags }), item.version) - } - - const params = this.args.filter || {} - if (this.args.children) { - this.show(await this.get(`/items/${this.args.key}/children`, { params })) - } else { - this.show(await this.get(`/items/${this.args.key}`, { params })) - } - } - - async $attachment(argparser = null) { - /** - Retrieve/save file attachments for the item specified with --key KEY (API: /items/KEY/file). - Also see 'item', which has options for adding/saving file attachments. - */ - - if (argparser) { - argparser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) - argparser.addArgument('--save', { required: true, help: 'Filename to save attachment to.' }) - return - } - - if (this.args.key) { - this.args.key = this.extractKeyAndSetGroup(this.args.key) - if (!this.args.key) { - this.parser.error('Unable to extract group/key from the string provided.') - return - } - } - - fs.writeFileSync(this.args.save, await this.get(`/items/${this.args.key}/file`), 'binary') - } - - async $create_item(argparser = null) { - /** - Create a new item or items. (API: /items/new) You can retrieve a template with the --template option. - - Use this option to create both top-level items, as well as child items (including notes and links). - */ - - if (argparser) { - argparser.addArgument('--template', { help: "Retrieve a template for the item you wish to create. You can retrieve the template types using the main argument 'types'." }) - argparser.addArgument('items', { nargs: '*', help: 'Json files for the items to be created.' }) - return - } - - if (this.args.template) { - this.show(await this.get('/items/new', { userOrGroupPrefix: false, params: { itemType: this.args.template } })) - return - } - - if (!this.args.items.length) this.parser.error('Need at least one item to create') - - const items = this.args.items.map(item => JSON.parse(fs.readFileSync(item, 'utf-8'))) - this.print(await this.post('/items', JSON.stringify(items))) - } - - async $update_item(argparser = null) { - /** Update/replace an item (--key KEY), either update (API: patch /items/KEY) or replacing (using --replace, API: put /items/KEY). */ - - if (argparser) { - argparser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) - argparser.addArgument('--replace', { action: 'storeTrue', help: 'Replace the item by sumbitting the complete json.' }) - argparser.addArgument('items', { nargs: 1, help: 'Path of item files in json format.' }) - return - } - - if (this.args.key) { - this.args.key = this.extractKeyAndSetGroup(this.args.key) - if (!this.args.key) { - this.parser.error('Unable to extract group/key from the string provided.') - return - } - } - - const originalItem = await this.get(`/items/${this.args.key}`) - for (const item of this.args.items) { - await this[this.args.replace ? 'put' : 'patch'](`/items/${this.args.key}`, fs.readFileSync(item), originalItem.version) - } - } - - // /items/trash Items in the trash - - async $trash(argparser = null) { - /** Return a list of items in the trash. */ - - if (argparser) return - - const items = await this.get('/items/trash') - this.show(items) - } - - - // https://www.zotero.org/support/dev/web_api/v3/basics - // /publications/items Items in My Publications - - async $publications(argparser = null) { - /** Return a list of items in publications (user library only). (API: /publications/items) */ - - if (argparser) return - - const items = await this.get('/publications/items') - this.show(items) - } - - // itemTypes - - async $types(argparser = null) { - /** Retrieve a list of items types available in Zotero. (API: /itemTypes) */ - - if (argparser) return - - this.show(await this.get('/itemTypes', { userOrGroupPrefix: false })) - } - - async $groups(argparser = null) { - /** Retrieve the Zotero groups data to which the current library_id and api_key has access to. (API: /users//groups) */ - if (argparser) return - - this.show(await this.get('/groups')) - } - - async $fields(argparser = null) { - /** - * Retrieve a template with the fields for --type TYPE (API: /itemTypeFields, /itemTypeCreatorTypes) or all item fields (API: /itemFields). - * Note that to retrieve a template, use 'create-item --template TYPE' rather than this command. - */ - - if (argparser) { - argparser.addArgument('--type', { help: 'Display fields types for TYPE.' }) - return - } - - if (this.args.type) { - this.show(await this.get('/itemTypeFields', { params: { itemType: this.args.type }, userOrGroupPrefix: false })) - this.show(await this.get('/itemTypeCreatorTypes', { params: { itemType: this.args.type }, userOrGroupPrefix: false })) - } else { - this.show(await this.get('/itemFields', { userOrGroupPrefix: false })) - } - } - - // Searches - // https://www.zotero.org/support/dev/web_api/v3/basics - - async $searches(argparser = null) { - /** Return a list of the saved searches of the library. Create new saved searches. (API: /searches) */ - - if (argparser) { - argparser.addArgument('--create', { nargs: 1, help: 'Path of JSON file containing the definitions of saved searches.' }) - return - } - - if (this.args.create) { - let searchDef = []; - try { - searchDef = JSON.parse(fs.readFileSync(this.args.create[0], 'utf8')) - } catch (ex) { - console.log('Invalid search definition: ', ex) - } - - if (!Array.isArray(searchDef)) { - searchDef = [searchDef] - } - - await this.post('/searches', JSON.stringify(searchDef)) - this.print('Saved search(s) created successfully.') - return - } - - const items = await this.get('/searches') - this.show(items) - } - - // Tags - - async $tags(argparser = null) { - /** Return a list of tags in the library. Options to filter and count tags. (API: /tags) */ - - if (argparser) { - argparser.addArgument('--filter', { help: 'Tags of all types matching a specific name.' }) - argparser.addArgument('--count', { action: 'storeTrue', help: 'TODO: document' }) - return - } - - let rawTags = null; - if (this.args.filter) { - rawTags = await this.all(`/tags/${encodeURIComponent(this.args.filter)}`) - } else { - rawTags = await this.all('/tags') - } - const tags = rawTags.map(tag => tag.tag).sort() - - if (this.args.count) { - const tag_counts: Record = {} - for (const tag of tags) { - tag_counts[tag] = await this.count('/items', { tag }) - } - this.print(tag_counts) - - } else { - this.show(tags) - } - } - // Other URLs // https://www.zotero.org/support/dev/web_api/v3/basics // /keys/ From 9eacd6388ee164eb7c667ba2e34c59eedbd528c1 Mon Sep 17 00:00:00 2001 From: reyamme <47262195+reyamme@users.noreply.github.com> Date: Tue, 29 Dec 2020 21:00:55 +0000 Subject: [PATCH 02/10] install libraries + fixing tsconfig + adding api+ts to import zotero class + fixing the code for all async functions --- bin/zotero-api-lib.ts | 874 ++++++++++++++++++++++++++++++++++++++++++ bin/zotero-cli.d.ts | 13 +- bin/zotero-cli.ts | 116 +++--- package-lock.json | 120 +++++- package.json | 7 +- tsconfig.json | 11 +- 6 files changed, 1077 insertions(+), 64 deletions(-) create mode 100644 bin/zotero-api-lib.ts diff --git a/bin/zotero-api-lib.ts b/bin/zotero-api-lib.ts new file mode 100644 index 0000000..463be94 --- /dev/null +++ b/bin/zotero-api-lib.ts @@ -0,0 +1,874 @@ +#!/usr/bin/env node + +require('dotenv').config(); +require('docstring'); +const os = require('os'); + +// import { ArgumentParser } from 'argparse' +const { ArgumentParser } = require('argparse'); +const TOML = require('@iarna/toml'); +const fs = require('fs'); +const path = require('path'); +const request = require('request-promise'); +const { LinkHeader } = require('http-link-header'); +const Ajv = require('ajv'); +const { parse } = require("args-any"); + + +// import { parse as TOML } from '@iarna/toml' +// import fs = require('fs') +// import path = require('path') + +// import request = require('request-promise') +// import * as LinkHeader from 'http-link-header' + +// import Ajv = require('ajv') +const ajv = new Ajv() + +const md5 = require('md5-file') + +function sleep(msecs) { + return new Promise(resolve => setTimeout(resolve, msecs)) +} + +const arg = new class { + integer(v) { + if (isNaN(parseInt(v))) throw new Error(`${JSON.stringify(v)} is not an integer`) + return parseInt(v) + } + + file(v) { + if (!fs.existsSync(v) || !fs.lstatSync(v).isFile()) throw new Error(`${JSON.stringify(v)} is not a file`) + return v + } + + path(v) { + if (!fs.existsSync(v)) throw new Error(`${JSON.stringify(v)} does not exist`) + return v + } + + json(v) { + return JSON.parse(v) + } +} + +export class Zotero { + args: any + output: string = '' + parser: any + config: any + zotero: any + base = 'https://api.zotero.org' + headers = { + 'User-Agent': 'Zotero-CLI', + 'Zotero-API-Version': '3', + } + + async run() { + this.output = '' + // global parameters for all commands + this.parser = new ArgumentParser + this.parser.addArgument('--api-key', { help: 'The API key to access the Zotero API.' }) + this.parser.addArgument('--config', { type: arg.file, help: 'Configuration file (toml format). Note that ./zotero-cli.toml and ~/.config/zotero-cli/zotero-cli.toml is picked up automatically.' }) + this.parser.addArgument('--user-id', { type: arg.integer, help: 'The id of the user library.' }) + this.parser.addArgument('--group-id', { type: arg.integer, help: 'The id of the group library.' }) + // See below. If changed, add: You can provide the group-id as zotero-select link (zotero://...). Only the group-id is used, the item/collection id is discarded. + this.parser.addArgument('--indent', { type: arg.integer, help: 'Identation for json output.' }) + this.parser.addArgument('--out', { help: 'Output to file' }) + this.parser.addArgument('--verbose', { action: 'storeTrue', help: 'Log requests.' }) + + const subparsers = this.parser.addSubparsers({ title: 'commands', dest: 'command', required: true }) + // add all methods that do not start with _ as a command + for (const cmd of Object.getOwnPropertyNames(Object.getPrototypeOf(this)).sort()) { + if (typeof this[cmd] !== 'function' || cmd[0] !== '$') continue + + const sp = subparsers.addParser(cmd.slice(1).replace(/_/g, '-'), { description: this[cmd].__doc__, help: this[cmd].__doc__ }) + // when called with an argparser, the command is expected to add relevant parameters and return + // the command must have a docstring + this[cmd](sp) + } + + this.args = this.parser.parseArgs() + + // pick up config + const config: string = [this.args.config, 'zotero-cli.toml', `${os.homedir()}/.config/zotero-cli/zotero-cli.toml`].find(cfg => fs.existsSync(cfg)) + this.config = config ? TOML(fs.readFileSync(config, 'utf-8')) : {} + + if (this.args.user_id || this.args.group_id) { + //Overwriting command line option in config + delete this.config['user-id'] + delete this.config['group-id'] + + this.config['user-id'] = this.args.user_id + this.config['group-id'] = this.args.group_id + + if (!this.config['user-id']) delete this.config['user-id'] + if (!this.config['group-id']) delete this.config['group-id'] + } + + // expand selected command + const options = [].concat.apply([], this.parser._actions.map(action => action.dest === 'command' ? action.choices[this.args.command] : [action])) + for (const option of options) { + if (!option.dest) continue + if (['help', 'config'].includes(option.dest)) continue + + if (this.args[option.dest] !== null) continue + + let value + + // first try explicit config + if (typeof value === 'undefined' && this.args.config) { + value = (this.config[this.args.command] || {})[option.dest.replace(/_/g, '-')] + if (typeof value === 'undefined') value = this.config[option.dest.replace(/_/g, '-')] + } + + // next, ENV vars. Also picks up from .env + if (typeof value === 'undefined') { + value = process.env[`ZOTERO_CLI_${option.dest.toUpperCase()}`] || process.env[`ZOTERO_${option.dest.toUpperCase()}`] + } + + // last, implicit config + if (typeof value === 'undefined') { + value = (this.config[this.args.command] || {})[option.dest.replace(/_/g, '-')] + if (typeof value === 'undefined') value = this.config[option.dest.replace(/_/g, '-')] + } + + if (typeof value === 'undefined') continue + + if (option.type === arg.integer) { + if (isNaN(parseInt(value))) this.parser.error(`${option.dest} must be numeric, not ${value}`) + value = parseInt(value) + + } else if (option.type === arg.path) { + if (!fs.existsSync(value)) this.parser.error(`${option.dest}: ${value} does not exist`) + + } else if (option.type === arg.file) { + if (!fs.existsSync(value) || !fs.lstatSync(value).isFile()) this.parser.error(`${option.dest}: ${value} is not a file`) + + } else if (option.type === arg.json && typeof value === 'string') { + try { + value = JSON.parse(value) + } catch (err) { + this.parser.error(`${option.dest}: ${JSON.stringify(value)} is not valid JSON`) + } + + } else if (option.choices) { + if (!option.choices.includes(value)) this.parser.error(`${option.dest} must be one of ${option.choices}`) + + } else if (option.action === 'storeTrue' && typeof value === 'string') { + const _value = { + true: true, + yes: true, + on: true, + + false: false, + no: false, + off: false, + }[value] + if (typeof _value === 'undefined') this.parser.error(`%{option.dest} must be boolean, not ${value}`) + value = _value + + } else { + // string + } + + this.args[option.dest] = value + } + + if (!this.args.api_key) this.parser.error('no API key provided') + this.headers['Zotero-API-Key'] = this.args.api_key + + if (this.args.user_id === null && this.args.group_id === null) this.parser.error('You must provide exactly one of --user-id or --group-id') + if (this.args.user_id !== null && this.args.group_id !== null) this.parser.error('You must provide exactly one of --user-id or --group-id') + if (this.args.user_id === 0) this.args.user_id = (await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })).userID + + + /* + // Could do this here: + if (this.args.group_id) { + this.args.group_id = this.extractGroup(this.args.group_id) + if (!this.args.group_id) { + this.parser.error('Unable to extract group_id from the string provided via --group_id.') + return + } + } + */ + + // using default=2 above prevents the overrides from being picked up + if (this.args.indent === null) this.args.indent = 2 + + // call the actual command + try { + await this['$' + this.args.command.replace(/-/g, '_')]() + } catch (ex) { + this.print('Command execution failed: ', ex) + process.exit(1) + } + + if (this.args.out) fs.writeFileSync(this.args.out, this.output) + } + + public print(...args: any[]) { + if (!this.args.out) { + console.log.apply(console, args) + + } else { + this.output += args.map(m => { + const type = typeof m + + if (type === 'string' || m instanceof String || type === 'number' || type === 'undefined' || type === 'boolean' || m === null) return m + + if (m instanceof Error) return `` + + if (m && type === 'object' && m.message) return `` + + return JSON.stringify(m, null, this.args.indent) + + }).join(' ') + '\n' + } + } + + async all(uri, params = {}) { + let chunk = await this.get(uri, { resolveWithFullResponse: true, params }) + let data = chunk.body + + let link = chunk.headers.link && LinkHeader.parse(chunk.headers.link).rel('next') + while (link && link.length && link[0].uri) { + if (chunk.headers.backoff) await sleep(parseInt(chunk.headers.backoff) * 1000) + + chunk = await request({ + uri: link[0].uri, + headers: this.headers, + json: true, + resolveWithFullResponse: true, + }) + data = data.concat(chunk.body) + link = chunk.headers.link && LinkHeader.parse(chunk.headers.link).rel('next') + } + return data + } + + // The Zotero API uses several commands: get, post, patch, delete - these are defined below. + + async get(uri, options: { userOrGroupPrefix?: boolean, params?: any, resolveWithFullResponse?: boolean, json?: boolean } = {}) { + if (typeof options.userOrGroupPrefix === 'undefined') options.userOrGroupPrefix = true + if (typeof options.params === 'undefined') options.params = {} + if (typeof options.json === 'undefined') options.json = true + + let prefix = '' + if (options.userOrGroupPrefix) prefix = this.args.user_id ? `/users/${this.args.user_id}` : `/groups/${this.args.group_id}` + + const params = Object.keys(options.params).map(param => { + let values = options.params[param] + if (!Array.isArray(values)) values = [values] + return values.map(v => `${param}=${encodeURI(v)}`).join('&') + }).join('&') + + uri = `${this.base}${prefix}${uri}${params ? '?' + params : ''}` + if (this.args.verbose) console.error('GET', uri) + + return request({ + uri, + headers: this.headers, + encoding: null, + json: options.json, + resolveWithFullResponse: options.resolveWithFullResponse, + }) + } + + async post(uri, data, headers = {}) { + const prefix = this.args.user_id ? `/users/${this.args.user_id}` : `/groups/${this.args.group_id}` + + uri = `${this.base}${prefix}${uri}` + if (this.args.verbose) console.error('POST', uri) + + return request({ + method: 'POST', + uri, + headers: { ...this.headers, 'Content-Type': 'application/json', ...headers }, + body: data, + }) + } + + async put(uri, data) { + const prefix = this.args.user_id ? `/users/${this.args.user_id}` : `/groups/${this.args.group_id}` + + uri = `${this.base}${prefix}${uri}` + if (this.args.verbose) console.error('PUT', uri) + + return request({ + method: 'PUT', + uri, + headers: { ...this.headers, 'Content-Type': 'application/json' }, + body: data, + }) + } + + async patch(uri, data, version?: number) { + const prefix = this.args.user_id ? `/users/${this.args.user_id}` : `/groups/${this.args.group_id}` + + const headers = { ...this.headers, 'Content-Type': 'application/json' } + if (typeof version !== 'undefined') headers['If-Unmodified-Since-Version'] = version + + uri = `${this.base}${prefix}${uri}` + if (this.args.verbose) console.error('PATCH', uri) + + return request({ + method: 'PATCH', + uri, + headers, + body: data, + }) + } + + async delete(uri, version?: number) { + const prefix = this.args.user_id ? `/users/${this.args.user_id}` : `/groups/${this.args.group_id}` + + const headers = { ...this.headers, 'Content-Type': 'application/json' } + if (typeof version !== 'undefined') headers['If-Unmodified-Since-Version'] = version + + uri = `${this.base}${prefix}${uri}` + if (this.args.verbose) console.error('DELETE', uri) + + return request({ + method: 'DELETE', + uri, + headers, + }) + } + + async count(uri, params = {}) { + return (await this.get(uri, { resolveWithFullResponse: true, params })).headers['total-results'] + } + + show(v) { + this.print(JSON.stringify(v, null, this.args.indent).replace(new RegExp(this.args.api_key, 'g'), '')) + } + + extractKeyAndSetGroup(key) { + // zotero://select/groups/(\d+)/(items|collections)/([A-Z01-9]+) + var out = key; + var res = key.match(/^zotero\:\/\/select\/groups\/(library|\d+)\/(items|collections)\/([A-Z01-9]+)/) + if (res) { + if (res[2] == "library") { + console.log('You cannot specify zotero-select links (zotero://...) to select user libraries.') + return + } else { + // console.log("Key: zotero://-key provided for "+res[2]+" Setting group-id.") + this.args.group_id = res[1] + out = res[3] + }; + } + return out + } + + /// THE COMMANDS /// + // The following functions define key API commands: /keys, /collection, /collections, etc. + + // https://www.zotero.org/support/dev/web_api/v3/basics + // Collections + // /collections Collections in the library + // /collections/top Top-level collections in the library + // /collections/ A specific collection in the library + // /collections//collections Subcollections within a specific collection in the library + + // TODO: --create-child should go into 'collection'. + + async $collections(argparser = null) { + /** Retrieve a list of collections or create a collection. (API: /collections, /collections/top, /collections//collections). Use 'collections --help' for details. */ + + if (argparser) { + argparser.addArgument('--top', { action: 'storeTrue', help: 'Show only collection at top level.' }) + argparser.addArgument('--key', { help: 'Show all the child collections of collection with key. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + argparser.addArgument('--create-child', { nargs: '*', help: 'Create child collections of key (or at the top level if no key is specified) with the names specified.' }) + return + } + + if (this.args.key) { + this.args.key = this.extractKeyAndSetGroup(this.args.key) + if (!this.args.key) { + this.parser.error('Unable to extract group/key from the string provided.') + return + } + } + + if (this.args.create_child) { + const response = await this.post('/collections', + JSON.stringify(this.args.create_child.map(c => { return { name: c, parentCollection: this.args.key } }))) + this.print('Collections created: ', JSON.parse(response).successful) + return + } + + + let collections = null; + if (this.args.key) { + collections = await this.all(`/collections/${this.args.key}/collections`) + } else { + collections = await this.all(`/collections${this.args.top ? '/top' : ''}`) + } + + this.show(collections) + } + + // Operate on a specific collection. + // /collections//items Items within a specific collection in the library + // /collections//items/top Top-level items within a specific collection in the library + + // TODO: --create-child should go into 'collection'. + // DONE: Why is does the setup for --add and --remove differ? Should 'add' not be "nargs: '*'"? Remove 'itemkeys'? + // TODO: Add option "--output file.json" to pipe output to file. + + async $collection(argparser = null) { + /** + Retrieve information about a specific collection --key KEY (API: /collections/KEY or /collections/KEY/tags). Use 'collection --help' for details. + (Note: Retrieve items is a collection via 'items --collection KEY'.) + */ + + if (argparser) { + argparser.addArgument('--key', { required: true, help: 'The key of the collection (required). You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + argparser.addArgument('--tags', { action: 'storeTrue', help: 'Display tags present in the collection.' }) + // argparser.addArgument('itemkeys', { nargs: '*' , help: 'Item keys for items to be added or removed from this collection.'}) + argparser.addArgument('--add', { nargs: '*', help: 'Add items to this collection. Note that adding items to collections with \'item --addtocollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + argparser.addArgument('--remove', { nargs: '*', help: 'Convenience method: Remove items from this collection. Note that removing items from collections with \'item --removefromcollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + return + } + + if (this.args.key) { + this.args.key = this.extractKeyAndSetGroup(this.args.key) + if (!this.args.key) { + this.parser.error('Unable to extract group/key from the string provided.') + return + } + } + + if (this.args.tags && this.args.add) { + this.parser.error('--tags cannot be combined with --add') + return + } + if (this.args.tags && this.args.remove) { + this.parser.error('--tags cannot be combined with --remove') + return + } + /* + if (this.args.add && !this.args.itemkeys.length) { + this.parser.error('--add requires item keys') + return + } + if (!this.args.add && this.args.itemkeys.length) { + this.parser.error('unexpected item keys') + return + } + */ + if (this.args.add) { + for (const itemKey of this.args.add) { + const item = await this.get(`/items/${itemKey}`) + if (item.data.collections.includes(this.args.key)) continue + await this.patch(`/items/${itemKey}`, JSON.stringify({ collections: item.data.collections.concat(this.args.key) }), item.version) + } + } + + if (this.args.remove) { + for (const itemKey of this.args.remove) { + const item = await this.get(`/items/${itemKey}`) + const index = item.data.collections.indexOf(this.args.key) + if (index > -1) { + item.data.collections.splice(index, 1) + } + await this.patch(`/items/${itemKey}`, JSON.stringify({ collections: item.data.collections }), item.version) + } + } + + this.show(await this.get(`/collections/${this.args.key}${this.args.tags ? '/tags' : ''}`)) + } + + // URI Description + // https://www.zotero.org/support/dev/web_api/v3/basics + // /items All items in the library, excluding trashed items + // /items/top Top-level items in the library, excluding trashed items + + async $items(argparser = null) { + /** + Retrieve list of items from API. (API: /items, /items/top, /collections/COLLECTION/items/top). + Use 'items --help' for details. + By default, all items are retrieved. With --top or limit (via --filter) the default number of items are retrieved. + */ + + let items + + if (argparser) { + argparser.addArgument('--count', { action: 'storeTrue', help: 'Return the number of items.' }) + // argparser.addArgument('--all', { action: 'storeTrue', help: 'obsolete' }) + argparser.addArgument('--filter', { type: arg.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. For example: \'{"format": "json,bib", "limit": 100, "start": 100}\'.' }) + argparser.addArgument('--collection', { help: 'Retrive list of items for collection. You can provide the collection key as a zotero-select link (zotero://...) to also set the group-id.' }) + argparser.addArgument('--top', { action: 'storeTrue', help: 'Retrieve top-level items in the library/collection (excluding child items / attachments, excluding trashed items).' }) + argparser.addArgument('--validate', { type: arg.path, help: 'json-schema file for all itemtypes, or directory with schema files, one per itemtype.' }) + return + } + + if (this.args.count && this.args.validate) { + this.parser.error('--count cannot be combined with --validate') + return + } + + if (this.args.collection) { + this.args.collection = this.extractKeyAndSetGroup(this.args.collection) + if (!this.args.collection) { + this.parser.error('Unable to extract group/key from the string provided.') + return + } + } + + const collection = this.args.collection ? `/collections/${this.args.collection}` : '' + + if (this.args.count) { + this.print(await this.count(`${collection}/items${this.args.top ? '/top' : ''}`, this.args.filter || {})) + return + } + + const params = this.args.filter || {} + + if (this.args.top) { + // This should be all - there may be more than 100 items. + // items = await this.all(`${collection}/items/top`, { params }) + items = await this.all(`${collection}/items/top`, params) + } else if (params.limit) { + if (params.limit > 100) { + this.parser.error('You can only retrieve up to 100 items with with params.limit.') + return + } + items = await this.get(`${collection}/items`, { params }) + } else { + items = await this.all(`${collection}/items`, params) + } + + if (this.args.validate) { + if (!fs.existsSync(this.args.validate)) throw new Error(`${this.args.validate} does not exist`) + + const oneSchema = fs.lstatSync(this.args.validate).isFile() + + let validate = oneSchema ? ajv.compile(JSON.parse(fs.readFileSync(this.args.validate, 'utf-8'))) : null + + const validators = {} + // still a bit rudimentary + for (const item of items) { + if (!oneSchema) { + validate = validators[item.itemType] = validators[item.itemType] || ajv.compile(JSON.parse(fs.readFileSync(path.join(this.args.validate, `${item.itemType}.json`), 'utf-8'))) + } + + if (!validate(item)) this.show(validate.errors) + } + + } else { + this.show(items) + } + } + + // https://www.zotero.org/support/dev/web_api/v3/basics + // /items/ A specific item in the library + // /items//children Child items under a specific item + + async $item(argparser = null) { + /** + Retrieve an item (item --key KEY), save/add file attachments, retrieve children. Manage collections and tags. (API: /items/KEY/ or /items/KEY/children). + + Also see 'attachment', 'create' and 'update'. + */ + + if (argparser) { + argparser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + argparser.addArgument('--children', { action: 'storeTrue', help: 'Retrieve list of children for the item.' }) + argparser.addArgument('--filter', { type: arg.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. To retrieve multiple items you have use "itemkey"; for example: \'{"format": "json,bib", "itemkey": "A,B,C"}\'. See https://www.zotero.org/support/dev/web_api/v3/basics#search_syntax.' }) + argparser.addArgument('--addfile', { nargs: '*', help: 'Upload attachments to the item. (/items/new)' }) + argparser.addArgument('--savefiles', { nargs: '*', help: 'Download all attachments from the item (/items/KEY/file).' }) + argparser.addArgument('--addtocollection', { nargs: '*', help: 'Add item to collections. (Convenience method: patch item->data->collections.)' }) + argparser.addArgument('--removefromcollection', { nargs: '*', help: 'Remove item from collections. (Convenience method: patch item->data->collections.)' }) + argparser.addArgument('--addtags', { nargs: '*', help: 'Add tags to item. (Convenience method: patch item->data->tags.)' }) + argparser.addArgument('--removetags', { nargs: '*', help: 'Remove tags from item. (Convenience method: patch item->data->tags.)' }) + return + } + + + if (this.args.key) { + this.args.key = this.extractKeyAndSetGroup(this.args.key) + if (!this.args.key) { + this.parser.error('Unable to extract group/key from the string provided.') + return + } + } + + const item = await this.get(`/items/${this.args.key}`) + + if (this.args.savefiles) { + let children = await this.get(`/items/${this.args.key}/children`); + await Promise.all(children.filter(item => item.data.itemType === 'attachment').map(async item => { + console.log(`Downloading file ${item.data.filename}`) + fs.writeFileSync(item.data.filename, await this.get(`/items/${item.key}/file`), 'binary') + })) + } + + if (this.args.addfile) { + const attachmentTemplate = await this.get('/items/new?itemType=attachment&linkMode=imported_file', { userOrGroupPrefix: false }) + for (const filename of this.args.addfile) { + if (!fs.existsSync(filename)) { + console.log(`Ignoring non-existing file: ${filename}`); + return + } + + let attach = attachmentTemplate; + attach.title = path.basename(filename) + attach.filename = path.basename(filename) + attach.contentType = `application/${path.extname(filename).slice(1)}` + attach.parentItem = this.args.key + const stat = fs.statSync(filename) + const uploadItem = JSON.parse(await this.post('/items', JSON.stringify([attach]))) + const uploadAuth = JSON.parse(await this.post(`/items/${uploadItem.successful[0].key}/file?md5=${md5.sync(filename)}&filename=${attach.filename}&filesize=${fs.statSync(filename)['size']}&mtime=${stat.mtimeMs}`, '{}', { 'If-None-Match': '*' })) + if (uploadAuth.exists !== 1) { + const uploadResponse = await request({ + method: 'POST', + uri: uploadAuth.url, + body: Buffer.concat([Buffer.from(uploadAuth.prefix), fs.readFileSync(filename), Buffer.from(uploadAuth.suffix)]), + headers: { 'Content-Type': uploadAuth.contentType } + }) + await this.post(`/items/${uploadItem.successful[0].key}/file?upload=${uploadAuth.uploadKey}`, '{}', { 'Content-Type': 'application/x-www-form-urlencoded', 'If-None-Match': '*' }) + } + } + } + + if (this.args.addtocollection) { + let newCollections = item.data.collections + this.args.addtocollection.forEach(itemKey => { + if (!newCollections.includes(itemKey)) { + newCollections.push(itemKey) + } + }) + await this.patch(`/items/${this.args.key}`, JSON.stringify({ collections: newCollections }), item.version) + } + + if (this.args.removefromcollection) { + let newCollections = item.data.collections + this.args.removefromcollection.forEach(itemKey => { + const index = newCollections.indexOf(itemKey) + if (index > -1) { + newCollections.splice(index, 1) + } + }) + await this.patch(`/items/${this.args.key}`, JSON.stringify({ collections: newCollections }), item.version) + } + + if (this.args.addtags) { + let newTags = item.data.tags + this.args.addtags.forEach(tag => { + if (!newTags.find(newTag => newTag.tag === tag)) { + newTags.push({ tag }) + } + }) + await this.patch(`/items/${this.args.key}`, JSON.stringify({ tags: newTags }), item.version) + } + + if (this.args.removetags) { + let newTags = item.data.tags.filter(tag => !this.args.removetags.includes(tag.tag)) + await this.patch(`/items/${this.args.key}`, JSON.stringify({ tags: newTags }), item.version) + } + + const params = this.args.filter || {} + if (this.args.children) { + this.show(await this.get(`/items/${this.args.key}/children`, { params })) + } else { + this.show(await this.get(`/items/${this.args.key}`, { params })) + } + } + + async $attachment(argparser = null) { + /** + Retrieve/save file attachments for the item specified with --key KEY (API: /items/KEY/file). + Also see 'item', which has options for adding/saving file attachments. + */ + + if (argparser) { + argparser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + argparser.addArgument('--save', { required: true, help: 'Filename to save attachment to.' }) + return + } + + if (this.args.key) { + this.args.key = this.extractKeyAndSetGroup(this.args.key) + if (!this.args.key) { + this.parser.error('Unable to extract group/key from the string provided.') + return + } + } + + fs.writeFileSync(this.args.save, await this.get(`/items/${this.args.key}/file`), 'binary') + } + + async $create_item(argparser = null) { + /** + Create a new item or items. (API: /items/new) You can retrieve a template with the --template option. + + Use this option to create both top-level items, as well as child items (including notes and links). + */ + + if (argparser) { + argparser.addArgument('--template', { help: "Retrieve a template for the item you wish to create. You can retrieve the template types using the main argument 'types'." }) + argparser.addArgument('items', { nargs: '*', help: 'Json files for the items to be created.' }) + return + } + + if (this.args.template) { + this.show(await this.get('/items/new', { userOrGroupPrefix: false, params: { itemType: this.args.template } })) + return + } + + if (!this.args.items.length) this.parser.error('Need at least one item to create') + + const items = this.args.items.map(item => JSON.parse(fs.readFileSync(item, 'utf-8'))) + this.print(await this.post('/items', JSON.stringify(items))) + } + + async $update_item(argparser = null) { + /** Update/replace an item (--key KEY), either update (API: patch /items/KEY) or replacing (using --replace, API: put /items/KEY). */ + + if (argparser) { + argparser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + argparser.addArgument('--replace', { action: 'storeTrue', help: 'Replace the item by sumbitting the complete json.' }) + argparser.addArgument('items', { nargs: 1, help: 'Path of item files in json format.' }) + return + } + + if (this.args.key) { + this.args.key = this.extractKeyAndSetGroup(this.args.key) + if (!this.args.key) { + this.parser.error('Unable to extract group/key from the string provided.') + return + } + } + + const originalItem = await this.get(`/items/${this.args.key}`) + for (const item of this.args.items) { + await this[this.args.replace ? 'put' : 'patch'](`/items/${this.args.key}`, fs.readFileSync(item), originalItem.version) + } + } + + // /items/trash Items in the trash + + async $trash(argparser = null) { + /** Return a list of items in the trash. */ + + if (argparser) return + + const items = await this.get('/items/trash') + this.show(items) + } + + + // https://www.zotero.org/support/dev/web_api/v3/basics + // /publications/items Items in My Publications + + async $publications(argparser = null) { + /** Return a list of items in publications (user library only). (API: /publications/items) */ + + if (argparser) return + + const items = await this.get('/publications/items') + this.show(items) + } + + // itemTypes + + async $types(argparser = null) { + /** Retrieve a list of items types available in Zotero. (API: /itemTypes) */ + + if (argparser) return + + this.show(await this.get('/itemTypes', { userOrGroupPrefix: false })) + } + + async $groups(argparser = null) { + /** Retrieve the Zotero groups data to which the current library_id and api_key has access to. (API: /users//groups) */ + if (argparser) return + + this.show(await this.get('/groups')) + } + + async $fields(argparser = null) { + /** + * Retrieve a template with the fields for --type TYPE (API: /itemTypeFields, /itemTypeCreatorTypes) or all item fields (API: /itemFields). + * Note that to retrieve a template, use 'create-item --template TYPE' rather than this command. + */ + + if (argparser) { + argparser.addArgument('--type', { help: 'Display fields types for TYPE.' }) + return + } + + if (this.args.type) { + this.show(await this.get('/itemTypeFields', { params: { itemType: this.args.type }, userOrGroupPrefix: false })) + this.show(await this.get('/itemTypeCreatorTypes', { params: { itemType: this.args.type }, userOrGroupPrefix: false })) + } else { + this.show(await this.get('/itemFields', { userOrGroupPrefix: false })) + } + } + + // Searches + // https://www.zotero.org/support/dev/web_api/v3/basics + + async $searches(argparser = null) { + /** Return a list of the saved searches of the library. Create new saved searches. (API: /searches) */ + + if (argparser) { + argparser.addArgument('--create', { nargs: 1, help: 'Path of JSON file containing the definitions of saved searches.' }) + return + } + + if (this.args.create) { + let searchDef = []; + try { + searchDef = JSON.parse(fs.readFileSync(this.args.create[0], 'utf8')) + } catch (ex) { + console.log('Invalid search definition: ', ex) + } + + if (!Array.isArray(searchDef)) { + searchDef = [searchDef] + } + + await this.post('/searches', JSON.stringify(searchDef)) + this.print('Saved search(s) created successfully.') + return + } + + const items = await this.get('/searches') + this.show(items) + } + + // Tags + + async $tags(argparser = null) { + /** Return a list of tags in the library. Options to filter and count tags. (API: /tags) */ + + if (argparser) { + argparser.addArgument('--filter', { help: 'Tags of all types matching a specific name.' }) + argparser.addArgument('--count', { action: 'storeTrue', help: 'TODO: document' }) + return + } + + let rawTags = null; + if (this.args.filter) { + rawTags = await this.all(`/tags/${encodeURIComponent(this.args.filter)}`) + } else { + rawTags = await this.all('/tags') + } + const tags = rawTags.map(tag => tag.tag).sort() + + if (this.args.count) { + const tag_counts: Record = {} + for (const tag of tags) { + tag_counts[tag] = await this.count('/items', { tag }) + } + this.print(tag_counts) + + } else { + this.show(tags) + } + } +} diff --git a/bin/zotero-cli.d.ts b/bin/zotero-cli.d.ts index b798801..583d6f1 100644 --- a/bin/zotero-cli.d.ts +++ b/bin/zotero-cli.d.ts @@ -1,2 +1,11 @@ -#!/usr/bin/env node -export {}; +declare const os: any; +declare const TOML: any; +declare const fs: any; +declare const path: any; +declare const request: any; +declare const LinkHeader: any; +declare const ajv: any; +declare const parse: any; +declare var async: any; +declare const ArgumentParser: any, argparser: any; +declare const md5: any; diff --git a/bin/zotero-cli.ts b/bin/zotero-cli.ts index 3335a4e..72004ca 100755 --- a/bin/zotero-cli.ts +++ b/bin/zotero-cli.ts @@ -1,72 +1,92 @@ - // Other URLs - // https://www.zotero.org/support/dev/web_api/v3/basics - // /keys/ - // /users//groups - - async $key(argparser = null) { - /** Show details about this API key. (API: /keys ) */ +// Other URLs +// https://www.zotero.org/support/dev/web_api/v3/basics +// /keys/ +// /users//groups - if (argparser) return +require('dotenv').config(); +require('docstring'); +const os = require('os'); - this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) - } +const { Zotero } = require('./zotero-api-lib.ts'); + +import { ArgumentParser } from 'argparse' +const TOML = require('@iarna/toml'); +const fs = require('fs'); +const path = require('path'); +const request = require('request-promise'); +const { LinkHeader } = require('http-link-header'); +const ajv = require('ajv'); +const { parse } = require("args-any"); +var async = require("async"); +const { ArgumentParser, argparser } = require('argparse'); +const md5 = require('md5') + + + +async function $key(argparser = null) { + /** Show details about this API key. (API: /keys ) */ + + if (argparser) return + + this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) +} - // Functions for get, post, put, patch, delete. (Delete query to API with uri.) - - async $get(argparser = null) { - /** Make a direct query to the API using 'GET uri'. */ +// Functions for get, post, put, patch, delete. (Delete query to API with uri.) - if (argparser) { - argparser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) - argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) - return - } +async function $get(argparser = null) { + /** Make a direct query to the API using 'GET uri'. */ - for (const uri of this.args.uri) { - this.show(await this.get(uri, { userOrGroupPrefix: !this.args.root })) - } + if (argparser) { + argparser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) + argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + return } - async $post(argparser = null) { - /** Make a direct query to the API using 'POST uri [--data data]'. */ + for (const uri of this.args.uri) { + this.show(await this.get(uri, { userOrGroupPrefix: !this.args.root })) + } +} - if (argparser) { - argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) - argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) - return - } +async function $post(argparser = null) { + /** Make a direct query to the API using 'POST uri [--data data]'. */ - this.print(await this.post(this.args.uri, this.args.data)) + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return } - async $put(argparser = null) { - /** Make a direct query to the API using 'PUT uri [--data data]'. */ + this.print(await this.post(this.args.uri, this.args.data)) +} - if (argparser) { - argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) - argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) - return - } +async function $put(argparser = null) { + /** Make a direct query to the API using 'PUT uri [--data data]'. */ - this.print(await this.put(this.args.uri, this.args.data)) + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return } - async $delete(argparser = null) { - /** Make a direct delete query to the API using 'DELETE uri'. */ + this.print(await this.put(this.args.uri, this.args.data)) +}; - if (argparser) { - argparser.addArgument('uri', { nargs: '+', help: 'Request uri' }) - return - } +async function $delete(argparser = null) { + /** Make a direct delete query to the API using 'DELETE uri'. */ - for (const uri of this.args.uri) { - const response = await this.get(uri) - await this.delete(uri, response.version) - } + if (argparser) { + argparser.addArgument('uri', { nargs: '+', help: 'Request uri' }) + return + } + + for (const uri of this.args.uri) { + const response = await this.get(uri) + await this.delete(uri, response.version) } } + (new Zotero).run().catch(err => { console.error('error:', err) process.exit(1) diff --git a/package-lock.json b/package-lock.json index 3f06b5c..cbce900 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,9 +36,9 @@ "integrity": "sha512-FmuxfCuolpLl0AnQ2NHSzoUKWEJDFl63qXjzdoWBVyFCXzMGm1spBzk7LeHNoVCiWCF7mRVms9e6jEV9+MoPbg==" }, "@types/node": { - "version": "13.11.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.1.tgz", - "integrity": "sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g==", + "version": "13.13.38", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.38.tgz", + "integrity": "sha512-oxo8j9doh7ab9NwDA9bCeFfjHRF/uzk+fTljCy8lMjZ3YzZGAXNDKhTE3Byso/oy32UTUQIXB3HCVHu3d2T3xg==", "dev": true }, "ajv": { @@ -75,6 +75,27 @@ "sprintf-js": "~1.0.2" } }, + "args-any": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/args-any/-/args-any-1.2.1.tgz", + "integrity": "sha512-76yBAkUBbl1ZdjTtBSwbDGUAull3qJKGa+bAGR3FTFgo/LjD6eF02Uj/KQ0Pok6ieFcQJk1FS0LKfiT/onnGSQ==", + "requires": { + "index-of-any": "^1.3.0", + "lodash": "^4.17.20", + "lodash-decorators": "^6.0.1", + "lodash.escaperegexp": "^4.1.2", + "reflect-metadata": "^0.1.13", + "string-converter": "^1.2.1", + "tslib": "^2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -88,6 +109,11 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -160,6 +186,11 @@ "supports-color": "^5.3.0" } }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -200,6 +231,11 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -348,6 +384,21 @@ "sshpk": "^1.7.0" } }, + "index-of-any": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/index-of-any/-/index-of-any-1.3.1.tgz", + "integrity": "sha512-sdLqn9igK8ldj5v2NfboQL3vWS8PXR9NH7lAe4DyXj5sc+usqt/PCe50rXzLe6165BHJ4FscfzITs9VYS+KX3Q==", + "requires": { + "tslib": "^2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -364,6 +415,11 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -422,9 +478,27 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "lodash-decorators": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/lodash-decorators/-/lodash-decorators-6.0.1.tgz", + "integrity": "sha512-1M0YC8G3nFTkejZEk2ehyvryEdcqj6xATH+ybI8j53cLs/bKRsavaE//y7nz/A0vxEFhxYqev7vdWfsuTJ1AtQ==", + "requires": { + "tslib": "^1.9.2" + } + }, + "lodash-es": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.20.tgz", + "integrity": "sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA==" + }, + "lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" }, "make-error": { "version": "1.3.6", @@ -432,6 +506,16 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "md5-file": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz", @@ -520,6 +604,11 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, "request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -644,6 +733,22 @@ "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" }, + "string-converter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/string-converter/-/string-converter-1.2.1.tgz", + "integrity": "sha512-12JDFZBjPMDUO85cEWQcLwCGhnfhHQPRdwPXDznu6tZABPqg694tW2w+8Msl6tt1/uP5GckEzDkkQb4kH7CYOA==", + "requires": { + "lodash-es": "^4.17.15", + "tslib": "^2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -685,8 +790,7 @@ "tslib": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", - "dev": true + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" }, "tslint": { "version": "6.1.1", diff --git a/package.json b/package.json index 9861d3e..96b05a7 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "homepage": "https://github.com/edtechhub/zotero-cli#readme", "devDependencies": { - "@types/node": "^13.11.1", + "@types/node": "^13.13.38", "ts-node": "^8.8.2", "tslint": "^6.1.1", "typescript": "^3.8.3" @@ -40,9 +40,12 @@ "@iarna/toml": "^2.2.3", "ajv": "^6.12.0", "argparse": "^1.0.10", + "args-any": "^1.2.1", + "async": "^3.2.0", "docstring": "^1.1.0", "dotenv": "^8.2.0", "http-link-header": "^1.0.2", + "md5": "^2.3.0", "md5-file": "^5.0.0", "request": "^2.88.2", "request-promise": "^4.2.5" @@ -54,4 +57,4 @@ "docs/COMMANDS.md", "README.md" ] -} +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index e663a0f..71eb358 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,18 +11,21 @@ "preserveConstEnums": false, "sourceMap": false, "downlevelIteration": true, - "lib": [ "es2017", "dom" ], + "lib": [ + "es2017", + "dom" + ], "typeRoots": [ "./typings", "./node_modules/@types" ] }, - "include": [ - "bin" + "files": [ + "bin/zotero-cli.ts" ], "exclude": [ "node_modules", "**/*.spec.ts", "typings" ] -} +} \ No newline at end of file From c8dffd0ea528c9196d9472b2f5af20eafc8889af Mon Sep 17 00:00:00 2001 From: reyamme <47262195+reyamme@users.noreply.github.com> Date: Tue, 29 Dec 2020 21:08:48 +0000 Subject: [PATCH 03/10] fixing_the_run_issues --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d80b82..a23b255 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,14 @@ A commandline tool to interact with the Zotero API. Developed by [@bjohas](https://github.com/bjohas), [@retorquere](https://github.com/retorquere) and [@a1diablo](https://github.com/a1diablo). -## Installation +Install this tool via +``` +npm install -g zotero-cli +``` + +This tool primarily parses the commandline option, while the API calls are made with https://github.com/OpenDevEd/zotero-api-lib-ts. + +## Installation from source ### node Run the following command to install dependencies From 46fc6b0fdd58215272f215e9ea253b8b1e1a4f07 Mon Sep 17 00:00:00 2001 From: reyamme <47262195+reyamme@users.noreply.github.com> Date: Wed, 30 Dec 2020 14:02:34 +0000 Subject: [PATCH 04/10] editing the code and run the args --- bin/zotero-api-lib.ts | 12 ++++++++---- bin/zotero-cli.d.ts | 12 +----------- package-lock.json | 5 +++++ package.json | 5 +++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/bin/zotero-api-lib.ts b/bin/zotero-api-lib.ts index 463be94..1723dc0 100644 --- a/bin/zotero-api-lib.ts +++ b/bin/zotero-api-lib.ts @@ -6,16 +6,18 @@ const os = require('os'); // import { ArgumentParser } from 'argparse' const { ArgumentParser } = require('argparse'); -const TOML = require('@iarna/toml'); +// var Toml = require('toml'); + +const toml = require('@iarna/toml'); const fs = require('fs'); const path = require('path'); const request = require('request-promise'); -const { LinkHeader } = require('http-link-header'); +const LinkHeader = require('http-link-header'); const Ajv = require('ajv'); const { parse } = require("args-any"); +// var toml = require('toml'); -// import { parse as TOML } from '@iarna/toml' // import fs = require('fs') // import path = require('path') @@ -64,6 +66,7 @@ export class Zotero { 'Zotero-API-Version': '3', } + //move it to cli async run() { this.output = '' // global parameters for all commands @@ -91,8 +94,9 @@ export class Zotero { this.args = this.parser.parseArgs() // pick up config + const config: string = [this.args.config, 'zotero-cli.toml', `${os.homedir()}/.config/zotero-cli/zotero-cli.toml`].find(cfg => fs.existsSync(cfg)) - this.config = config ? TOML(fs.readFileSync(config, 'utf-8')) : {} + this.config = config ? toml.parse(fs.readFileSync(config, 'utf-8')) : {} if (this.args.user_id || this.args.group_id) { //Overwriting command line option in config diff --git a/bin/zotero-cli.d.ts b/bin/zotero-cli.d.ts index 583d6f1..cb0ff5c 100644 --- a/bin/zotero-cli.d.ts +++ b/bin/zotero-cli.d.ts @@ -1,11 +1 @@ -declare const os: any; -declare const TOML: any; -declare const fs: any; -declare const path: any; -declare const request: any; -declare const LinkHeader: any; -declare const ajv: any; -declare const parse: any; -declare var async: any; -declare const ArgumentParser: any, argparser: any; -declare const md5: any; +export {}; diff --git a/package-lock.json b/package-lock.json index cbce900..9d64a67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -758,6 +758,11 @@ "has-flag": "^3.0.0" } }, + "toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", diff --git a/package.json b/package.json index 96b05a7..7988574 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "md5": "^2.3.0", "md5-file": "^5.0.0", "request": "^2.88.2", - "request-promise": "^4.2.5" + "request-promise": "^4.2.5", + "toml": "^3.0.0" }, "files": [ "package.json", @@ -57,4 +58,4 @@ "docs/COMMANDS.md", "README.md" ] -} \ No newline at end of file +} From 5a85618c6e9620d63175ca1f2fe4baf198a45836 Mon Sep 17 00:00:00 2001 From: reyamme <47262195+reyamme@users.noreply.github.com> Date: Wed, 30 Dec 2020 22:01:30 +0000 Subject: [PATCH 05/10] fixing the errors after sepearting the run function --- bin/zotero-api-lib.ts | 45 ++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/bin/zotero-api-lib.ts b/bin/zotero-api-lib.ts index 1723dc0..ebfe73f 100644 --- a/bin/zotero-api-lib.ts +++ b/bin/zotero-api-lib.ts @@ -6,18 +6,18 @@ const os = require('os'); // import { ArgumentParser } from 'argparse' const { ArgumentParser } = require('argparse'); -// var Toml = require('toml'); - const toml = require('@iarna/toml'); const fs = require('fs'); const path = require('path'); const request = require('request-promise'); -const LinkHeader = require('http-link-header'); +const { LinkHeader } = require('http-link-header'); const Ajv = require('ajv'); const { parse } = require("args-any"); -// var toml = require('toml'); + + +// import { parse as TOML } from '@iarna/toml' // import fs = require('fs') // import path = require('path') @@ -66,7 +66,6 @@ export class Zotero { 'Zotero-API-Version': '3', } - //move it to cli async run() { this.output = '' // global parameters for all commands @@ -94,7 +93,6 @@ export class Zotero { this.args = this.parser.parseArgs() // pick up config - const config: string = [this.args.config, 'zotero-cli.toml', `${os.homedir()}/.config/zotero-cli/zotero-cli.toml`].find(cfg => fs.existsSync(cfg)) this.config = config ? toml.parse(fs.readFileSync(config, 'utf-8')) : {} @@ -192,8 +190,8 @@ export class Zotero { if (this.args.group_id) { this.args.group_id = this.extractGroup(this.args.group_id) if (!this.args.group_id) { - this.parser.error('Unable to extract group_id from the string provided via --group_id.') - return + this.parser.error('Unable to extract group_id from the string provided via --group_id.') + return } } */ @@ -424,8 +422,8 @@ export class Zotero { async $collection(argparser = null) { /** - Retrieve information about a specific collection --key KEY (API: /collections/KEY or /collections/KEY/tags). Use 'collection --help' for details. - (Note: Retrieve items is a collection via 'items --collection KEY'.) + Retrieve information about a specific collection --key KEY (API: /collections/KEY or /collections/KEY/tags). Use 'collection --help' for details. + (Note: Retrieve items is a collection via 'items --collection KEY'.) */ if (argparser) { @@ -492,9 +490,9 @@ export class Zotero { async $items(argparser = null) { /** - Retrieve list of items from API. (API: /items, /items/top, /collections/COLLECTION/items/top). - Use 'items --help' for details. - By default, all items are retrieved. With --top or limit (via --filter) the default number of items are retrieved. + Retrieve list of items from API. (API: /items, /items/top, /collections/COLLECTION/items/top). + Use 'items --help' for details. + By default, all items are retrieved. With --top or limit (via --filter) the default number of items are retrieved. */ let items @@ -573,9 +571,9 @@ export class Zotero { async $item(argparser = null) { /** - Retrieve an item (item --key KEY), save/add file attachments, retrieve children. Manage collections and tags. (API: /items/KEY/ or /items/KEY/children). - - Also see 'attachment', 'create' and 'update'. + Retrieve an item (item --key KEY), save/add file attachments, retrieve children. Manage collections and tags. (API: /items/KEY/ or /items/KEY/children). + + Also see 'attachment', 'create' and 'update'. */ if (argparser) { @@ -684,8 +682,8 @@ export class Zotero { async $attachment(argparser = null) { /** - Retrieve/save file attachments for the item specified with --key KEY (API: /items/KEY/file). - Also see 'item', which has options for adding/saving file attachments. + Retrieve/save file attachments for the item specified with --key KEY (API: /items/KEY/file). + Also see 'item', which has options for adding/saving file attachments. */ if (argparser) { @@ -707,9 +705,9 @@ export class Zotero { async $create_item(argparser = null) { /** - Create a new item or items. (API: /items/new) You can retrieve a template with the --template option. - - Use this option to create both top-level items, as well as child items (including notes and links). + Create a new item or items. (API: /items/new) You can retrieve a template with the --template option. + + Use this option to create both top-level items, as well as child items (including notes and links). */ if (argparser) { @@ -874,5 +872,8 @@ export class Zotero { } else { this.show(tags) } + + } -} + +} \ No newline at end of file From 7387aebb5c48c26d64e60ff60a9e855303e9bd16 Mon Sep 17 00:00:00 2001 From: reyamme <47262195+reyamme@users.noreply.github.com> Date: Tue, 5 Jan 2021 22:36:08 +0000 Subject: [PATCH 06/10] seperation &rewrite the code and fixing the errors in the cli and api --- bin/zotero-api-lib.ts | 125 +++++++++++++++++++++--------------------- bin/zotero-cli.ts | 98 +++++++++++++++++++++++++++++++-- package-lock.json | 20 +++++-- package.json | 2 + 4 files changed, 174 insertions(+), 71 deletions(-) diff --git a/bin/zotero-api-lib.ts b/bin/zotero-api-lib.ts index ebfe73f..27882b3 100644 --- a/bin/zotero-api-lib.ts +++ b/bin/zotero-api-lib.ts @@ -4,8 +4,9 @@ require('dotenv').config(); require('docstring'); const os = require('os'); + // import { ArgumentParser } from 'argparse' -const { ArgumentParser } = require('argparse'); +const { ArgumentParser, parser } = require('argparse'); const toml = require('@iarna/toml'); const fs = require('fs'); const path = require('path'); @@ -13,6 +14,7 @@ const request = require('request-promise'); const { LinkHeader } = require('http-link-header'); const Ajv = require('ajv'); const { parse } = require("args-any"); +// const { parArg, re } = require('./zotero-cli.ts') @@ -33,26 +35,26 @@ function sleep(msecs) { return new Promise(resolve => setTimeout(resolve, msecs)) } -const arg = new class { - integer(v) { - if (isNaN(parseInt(v))) throw new Error(`${JSON.stringify(v)} is not an integer`) - return parseInt(v) - } +// const arg = new class { +// integer(v) { +// if (isNaN(parseInt(v))) throw new Error(`${JSON.stringify(v)} is not an integer`) +// return parseInt(v) +// } - file(v) { - if (!fs.existsSync(v) || !fs.lstatSync(v).isFile()) throw new Error(`${JSON.stringify(v)} is not a file`) - return v - } +// file(v) { +// if (!fs.existsSync(v) || !fs.lstatSync(v).isFile()) throw new Error(`${JSON.stringify(v)} is not a file`) +// return v +// } - path(v) { - if (!fs.existsSync(v)) throw new Error(`${JSON.stringify(v)} does not exist`) - return v - } +// path(v) { +// if (!fs.existsSync(v)) throw new Error(`${JSON.stringify(v)} does not exist`) +// return v +// } - json(v) { - return JSON.parse(v) - } -} +// json(v) { +// return JSON.parse(v) +// } +// } export class Zotero { args: any @@ -60,6 +62,7 @@ export class Zotero { parser: any config: any zotero: any + arg: any base = 'https://api.zotero.org' headers = { 'User-Agent': 'Zotero-CLI', @@ -69,28 +72,28 @@ export class Zotero { async run() { this.output = '' // global parameters for all commands - this.parser = new ArgumentParser - this.parser.addArgument('--api-key', { help: 'The API key to access the Zotero API.' }) - this.parser.addArgument('--config', { type: arg.file, help: 'Configuration file (toml format). Note that ./zotero-cli.toml and ~/.config/zotero-cli/zotero-cli.toml is picked up automatically.' }) - this.parser.addArgument('--user-id', { type: arg.integer, help: 'The id of the user library.' }) - this.parser.addArgument('--group-id', { type: arg.integer, help: 'The id of the group library.' }) - // See below. If changed, add: You can provide the group-id as zotero-select link (zotero://...). Only the group-id is used, the item/collection id is discarded. - this.parser.addArgument('--indent', { type: arg.integer, help: 'Identation for json output.' }) - this.parser.addArgument('--out', { help: 'Output to file' }) - this.parser.addArgument('--verbose', { action: 'storeTrue', help: 'Log requests.' }) - - const subparsers = this.parser.addSubparsers({ title: 'commands', dest: 'command', required: true }) - // add all methods that do not start with _ as a command - for (const cmd of Object.getOwnPropertyNames(Object.getPrototypeOf(this)).sort()) { - if (typeof this[cmd] !== 'function' || cmd[0] !== '$') continue - - const sp = subparsers.addParser(cmd.slice(1).replace(/_/g, '-'), { description: this[cmd].__doc__, help: this[cmd].__doc__ }) - // when called with an argparser, the command is expected to add relevant parameters and return - // the command must have a docstring - this[cmd](sp) - } - - this.args = this.parser.parseArgs() + // this.parser = new ArgumentParser + // this.parser.addArgument('--api-key', { help: 'The API key to access the Zotero API.' }) + // this.parser.addArgument('--config', { type: arg.file, help: 'Configuration file (toml format). Note that ./zotero-cli.toml and ~/.config/zotero-cli/zotero-cli.toml is picked up automatically.' }) + // this.parser.addArgument('--user-id', { type: arg.integer, help: 'The id of the user library.' }) + // this.parser.addArgument('--group-id', { type: arg.integer, help: 'The id of the group library.' }) + // // See below. If changed, add: You can provide the group-id as zotero-select link (zotero://...). Only the group-id is used, the item/collection id is discarded. + // this.parser.addArgument('--indent', { type: arg.integer, help: 'Identation for json output.' }) + // this.parser.addArgument('--out', { help: 'Output to file' }) + // this.parser.addArgument('--verbose', { action: 'storeTrue', help: 'Log requests.' }) + + // const subparsers = this.parser.addSubparsers({ title: 'commands', dest: 'command', required: true }) + // // add all methods that do not start with _ as a command + // for (const cmd of Object.getOwnPropertyNames(Object.getPrototypeOf(this)).sort()) { + // if (typeof this[cmd] !== 'function' || cmd[0] !== '$') continue + + // const sp = subparsers.addParser(cmd.slice(1).replace(/_/g, '-'), { description: this[cmd].__doc__, help: this[cmd].__doc__ }) + // // when called with an argparser, the command is expected to add relevant parameters and return + // // the command must have a docstring + // this[cmd](sp) + // } + + // this.args = this.parser.parseArgs() // pick up config const config: string = [this.args.config, 'zotero-cli.toml', `${os.homedir()}/.config/zotero-cli/zotero-cli.toml`].find(cfg => fs.existsSync(cfg)) @@ -137,17 +140,17 @@ export class Zotero { if (typeof value === 'undefined') continue - if (option.type === arg.integer) { + if (option.type === this.arg.integer) { if (isNaN(parseInt(value))) this.parser.error(`${option.dest} must be numeric, not ${value}`) value = parseInt(value) - } else if (option.type === arg.path) { + } else if (option.type === this.arg.path) { if (!fs.existsSync(value)) this.parser.error(`${option.dest}: ${value} does not exist`) - } else if (option.type === arg.file) { + } else if (option.type === this.arg.file) { if (!fs.existsSync(value) || !fs.lstatSync(value).isFile()) this.parser.error(`${option.dest}: ${value} is not a file`) - } else if (option.type === arg.json && typeof value === 'string') { + } else if (option.type === this.arg.json && typeof value === 'string') { try { value = JSON.parse(value) } catch (err) { @@ -379,12 +382,12 @@ export class Zotero { async $collections(argparser = null) { /** Retrieve a list of collections or create a collection. (API: /collections, /collections/top, /collections//collections). Use 'collections --help' for details. */ - if (argparser) { - argparser.addArgument('--top', { action: 'storeTrue', help: 'Show only collection at top level.' }) - argparser.addArgument('--key', { help: 'Show all the child collections of collection with key. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) - argparser.addArgument('--create-child', { nargs: '*', help: 'Create child collections of key (or at the top level if no key is specified) with the names specified.' }) - return - } + // if (argparser) { + // argparser.addArgument('--top', { action: 'storeTrue', help: 'Show only collection at top level.' }) + // argparser.addArgument('--key', { help: 'Show all the child collections of collection with key. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + // argparser.addArgument('--create-child', { nargs: '*', help: 'Create child collections of key (or at the top level if no key is specified) with the names specified.' }) + // return + // } if (this.args.key) { this.args.key = this.extractKeyAndSetGroup(this.args.key) @@ -426,14 +429,14 @@ export class Zotero { (Note: Retrieve items is a collection via 'items --collection KEY'.) */ - if (argparser) { - argparser.addArgument('--key', { required: true, help: 'The key of the collection (required). You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) - argparser.addArgument('--tags', { action: 'storeTrue', help: 'Display tags present in the collection.' }) - // argparser.addArgument('itemkeys', { nargs: '*' , help: 'Item keys for items to be added or removed from this collection.'}) - argparser.addArgument('--add', { nargs: '*', help: 'Add items to this collection. Note that adding items to collections with \'item --addtocollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) - argparser.addArgument('--remove', { nargs: '*', help: 'Convenience method: Remove items from this collection. Note that removing items from collections with \'item --removefromcollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) - return - } + // if (argparser) { + // argparser.addArgument('--key', { required: true, help: 'The key of the collection (required). You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + // argparser.addArgument('--tags', { action: 'storeTrue', help: 'Display tags present in the collection.' }) + // // argparser.addArgument('itemkeys', { nargs: '*' , help: 'Item keys for items to be added or removed from this collection.'}) + // argparser.addArgument('--add', { nargs: '*', help: 'Add items to this collection. Note that adding items to collections with \'item --addtocollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + // argparser.addArgument('--remove', { nargs: '*', help: 'Convenience method: Remove items from this collection. Note that removing items from collections with \'item --removefromcollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + // return + // } if (this.args.key) { this.args.key = this.extractKeyAndSetGroup(this.args.key) @@ -500,10 +503,10 @@ export class Zotero { if (argparser) { argparser.addArgument('--count', { action: 'storeTrue', help: 'Return the number of items.' }) // argparser.addArgument('--all', { action: 'storeTrue', help: 'obsolete' }) - argparser.addArgument('--filter', { type: arg.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. For example: \'{"format": "json,bib", "limit": 100, "start": 100}\'.' }) + argparser.addArgument('--filter', { type: this.arg.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. For example: \'{"format": "json,bib", "limit": 100, "start": 100}\'.' }) argparser.addArgument('--collection', { help: 'Retrive list of items for collection. You can provide the collection key as a zotero-select link (zotero://...) to also set the group-id.' }) argparser.addArgument('--top', { action: 'storeTrue', help: 'Retrieve top-level items in the library/collection (excluding child items / attachments, excluding trashed items).' }) - argparser.addArgument('--validate', { type: arg.path, help: 'json-schema file for all itemtypes, or directory with schema files, one per itemtype.' }) + argparser.addArgument('--validate', { type: this.arg.path, help: 'json-schema file for all itemtypes, or directory with schema files, one per itemtype.' }) return } @@ -579,7 +582,7 @@ export class Zotero { if (argparser) { argparser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) argparser.addArgument('--children', { action: 'storeTrue', help: 'Retrieve list of children for the item.' }) - argparser.addArgument('--filter', { type: arg.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. To retrieve multiple items you have use "itemkey"; for example: \'{"format": "json,bib", "itemkey": "A,B,C"}\'. See https://www.zotero.org/support/dev/web_api/v3/basics#search_syntax.' }) + argparser.addArgument('--filter', { type: this.arg.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. To retrieve multiple items you have use "itemkey"; for example: \'{"format": "json,bib", "itemkey": "A,B,C"}\'. See https://www.zotero.org/support/dev/web_api/v3/basics#search_syntax.' }) argparser.addArgument('--addfile', { nargs: '*', help: 'Upload attachments to the item. (/items/new)' }) argparser.addArgument('--savefiles', { nargs: '*', help: 'Download all attachments from the item (/items/KEY/file).' }) argparser.addArgument('--addtocollection', { nargs: '*', help: 'Add item to collections. (Convenience method: patch item->data->collections.)' }) diff --git a/bin/zotero-cli.ts b/bin/zotero-cli.ts index 72004ca..a90f7ac 100755 --- a/bin/zotero-cli.ts +++ b/bin/zotero-cli.ts @@ -22,10 +22,76 @@ var async = require("async"); const { ArgumentParser, argparser } = require('argparse'); const md5 = require('md5') +const arg = new class { + integer(v) { + if (isNaN(parseInt(v))) throw new Error(`${JSON.stringify(v)} is not an integer`) + return parseInt(v) + } + file(v) { + if (!fs.existsSync(v) || !fs.lstatSync(v).isFile()) throw new Error(`${JSON.stringify(v)} is not a file`) + return v + } + + path(v) { + if (!fs.existsSync(v)) throw new Error(`${JSON.stringify(v)} does not exist`) + return v + } + + json(v) { + return JSON.parse(v) + } +} + + +function parArg(api) { + + let parser = new ArgumentParser + // let argparser = new ArgumentParser + parser.addArgument('--api-key', { help: 'The API key to access the Zotero API.' }) + parser.addArgument('--config', { type: arg.file, help: 'Configuration file (toml format). Note that ./zotero-cli.toml and ~/.config/zotero-cli/zotero-cli.toml is picked up automatically.' }) + parser.addArgument('--user-id', { type: arg.integer, help: 'The id of the user library.' }) + parser.addArgument('--group-id', { type: arg.integer, help: 'The id of the group library.' }) + // See below. If changed, add: You can provide the group-id as zotero-select link (zotero://...). Only the group-id is used, the item/collection id is discarded. + parser.addArgument('--indent', { type: arg.integer, help: 'Identation for json output.' }) + parser.addArgument('--out', { help: 'Output to file' }) + parser.addArgument('--verbose', { action: 'storeTrue', help: 'Log requests.' }) + + const subparsers = parser.addSubparsers({ title: 'commands', dest: 'command', required: true }) + // add all methods that do not start with _ as a command + for (const cmd of Object.getOwnPropertyNames(Object.getPrototypeOf(api)).sort()) { + if (typeof api[cmd] !== 'function' || cmd[0] !== '$') continue + + const sp = subparsers.addParser(cmd.slice(1).replace(/_/g, '-'), { description: api[cmd].__doc__, help: api[cmd].__doc__ }) + // when called with an argparser, the command is expected to add relevant parameters and return + // the command must have a docstring + if (sp) { + if (cmd === "$collection") { + sp.addArgument('--key', { required: true, help: 'The key of the collection (required). You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + sp.addArgument('--tags', { action: 'storeTrue', help: 'Display tags present in the collection.' }) + // argparser.addArgument('itemkeys', { nargs: '*' , help: 'Item keys for items to be added or removed from this collection.'}) + sp.addArgument('--add', { nargs: '*', help: 'Add items to this collection. Note that adding items to collections with \'item --addtocollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + sp.addArgument('--remove', { nargs: '*', help: 'Convenience method: Remove items from this collection. Note that removing items from collections with \'item --removefromcollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + } + if (cmd === "$collections") { + sp.addArgument('--top', { action: 'storeTrue', help: 'Show only collection at top level.' }) + sp.addArgument('--key', { help: 'Show all the child collections of collection with key. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + sp.addArgument('--create-child', { nargs: '*', help: 'Create child collections of key (or at the top level if no key is specified) with the names specified.' }) + } + + } + else { + api[cmd](sp) + } + } + + return parser +} + + async function $key(argparser = null) { - /** Show details about this API key. (API: /keys ) */ +/** Show details about this API key. (API: /keys ) */ if (argparser) return @@ -36,13 +102,29 @@ async function $key(argparser = null) { async function $get(argparser = null) { /** Make a direct query to the API using 'GET uri'. */ +console.log("rrrrrrr") - if (argparser) { + if (argparser) { argparser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) - argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) +argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) return - } +} + + + + + + + + + + + + + + + for (const uri of this.args.uri) { this.show(await this.get(uri, { userOrGroupPrefix: !this.args.root })) } @@ -85,9 +167,13 @@ async function $delete(argparser = null) { await this.delete(uri, response.version) } } +const ee = new Zotero() +ee.arg = arg +ee.parser = parArg(ee) +ee.args = parArg(ee).parseArgs() +ee.run( - -(new Zotero).run().catch(err => { + ).catch(err => { console.error('error:', err) process.exit(1) }) diff --git a/package-lock.json b/package-lock.json index 9d64a67..0c096c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,10 +62,9 @@ } }, "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.0.tgz", + "integrity": "sha512-4P8Zm2H+BRS+c/xX1LrHw0qKpEhdlZjLCgWy+d78T9vqa2Z2SiD2wMrYuWIAFy5IZUD7nnNXroRttz+0RzlrzQ==" }, "argparse": { "version": "1.0.10", @@ -572,6 +571,11 @@ "wrappy": "1" } }, + "parseArgs": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/parseArgs/-/parseArgs-0.0.2.tgz", + "integrity": "sha1-YZpmltzE9/yQCFnxDr8SamCTClY=" + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -790,6 +794,14 @@ "make-error": "^1.1.1", "source-map-support": "^0.5.6", "yn": "3.1.1" + }, + "dependencies": { + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + } } }, "tslib": { diff --git a/package.json b/package.json index 7988574..8d87012 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "dependencies": { "@iarna/toml": "^2.2.3", "ajv": "^6.12.0", + "arg": "^5.0.0", "argparse": "^1.0.10", "args-any": "^1.2.1", "async": "^3.2.0", @@ -47,6 +48,7 @@ "http-link-header": "^1.0.2", "md5": "^2.3.0", "md5-file": "^5.0.0", + "parseArgs": "0.0.2", "request": "^2.88.2", "request-promise": "^4.2.5", "toml": "^3.0.0" From 094b8624783a20583245df61b66183beb84553a7 Mon Sep 17 00:00:00 2001 From: reyamme <47262195+reyamme@users.noreply.github.com> Date: Thu, 7 Jan 2021 17:12:02 +0000 Subject: [PATCH 07/10] sepration the rest code --- bin/zotero-api-lib.ts | 41 ++++++++++++++--------------------------- bin/zotero-cli.d.ts | 26 +++++++++++++++++++++++++- bin/zotero-cli.ts | 35 ++++++++++++++++++++++++++++++++--- 3 files changed, 71 insertions(+), 31 deletions(-) diff --git a/bin/zotero-api-lib.ts b/bin/zotero-api-lib.ts index 27882b3..aa5bf60 100644 --- a/bin/zotero-api-lib.ts +++ b/bin/zotero-api-lib.ts @@ -500,15 +500,15 @@ export class Zotero { let items - if (argparser) { - argparser.addArgument('--count', { action: 'storeTrue', help: 'Return the number of items.' }) - // argparser.addArgument('--all', { action: 'storeTrue', help: 'obsolete' }) - argparser.addArgument('--filter', { type: this.arg.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. For example: \'{"format": "json,bib", "limit": 100, "start": 100}\'.' }) - argparser.addArgument('--collection', { help: 'Retrive list of items for collection. You can provide the collection key as a zotero-select link (zotero://...) to also set the group-id.' }) - argparser.addArgument('--top', { action: 'storeTrue', help: 'Retrieve top-level items in the library/collection (excluding child items / attachments, excluding trashed items).' }) - argparser.addArgument('--validate', { type: this.arg.path, help: 'json-schema file for all itemtypes, or directory with schema files, one per itemtype.' }) - return - } + // if (argparser) { + // argparser.addArgument('--count', { action: 'storeTrue', help: 'Return the number of items.' }) + // // argparser.addArgument('--all', { action: 'storeTrue', help: 'obsolete' }) + // argparser.addArgument('--filter', { type: this.arg.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. For example: \'{"format": "json,bib", "limit": 100, "start": 100}\'.' }) + // argparser.addArgument('--collection', { help: 'Retrive list of items for collection. You can provide the collection key as a zotero-select link (zotero://...) to also set the group-id.' }) + // argparser.addArgument('--top', { action: 'storeTrue', help: 'Retrieve top-level items in the library/collection (excluding child items / attachments, excluding trashed items).' }) + // argparser.addArgument('--validate', { type: this.arg.path, help: 'json-schema file for all itemtypes, or directory with schema files, one per itemtype.' }) + // return + // } if (this.args.count && this.args.validate) { this.parser.error('--count cannot be combined with --validate') @@ -579,19 +579,6 @@ export class Zotero { Also see 'attachment', 'create' and 'update'. */ - if (argparser) { - argparser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) - argparser.addArgument('--children', { action: 'storeTrue', help: 'Retrieve list of children for the item.' }) - argparser.addArgument('--filter', { type: this.arg.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. To retrieve multiple items you have use "itemkey"; for example: \'{"format": "json,bib", "itemkey": "A,B,C"}\'. See https://www.zotero.org/support/dev/web_api/v3/basics#search_syntax.' }) - argparser.addArgument('--addfile', { nargs: '*', help: 'Upload attachments to the item. (/items/new)' }) - argparser.addArgument('--savefiles', { nargs: '*', help: 'Download all attachments from the item (/items/KEY/file).' }) - argparser.addArgument('--addtocollection', { nargs: '*', help: 'Add item to collections. (Convenience method: patch item->data->collections.)' }) - argparser.addArgument('--removefromcollection', { nargs: '*', help: 'Remove item from collections. (Convenience method: patch item->data->collections.)' }) - argparser.addArgument('--addtags', { nargs: '*', help: 'Add tags to item. (Convenience method: patch item->data->tags.)' }) - argparser.addArgument('--removetags', { nargs: '*', help: 'Remove tags from item. (Convenience method: patch item->data->tags.)' }) - return - } - if (this.args.key) { this.args.key = this.extractKeyAndSetGroup(this.args.key) @@ -689,11 +676,11 @@ export class Zotero { Also see 'item', which has options for adding/saving file attachments. */ - if (argparser) { - argparser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) - argparser.addArgument('--save', { required: true, help: 'Filename to save attachment to.' }) - return - } + // if (argparser) { + // argparser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + // argparser.addArgument('--save', { required: true, help: 'Filename to save attachment to.' }) + // return + // } if (this.args.key) { this.args.key = this.extractKeyAndSetGroup(this.args.key) diff --git a/bin/zotero-cli.d.ts b/bin/zotero-cli.d.ts index cb0ff5c..effcd35 100644 --- a/bin/zotero-cli.d.ts +++ b/bin/zotero-cli.d.ts @@ -1 +1,25 @@ -export {}; +declare const os: any; +declare const Zotero: any; +declare const TOML: any; +declare const fs: any; +declare const path: any; +declare const request: any; +declare const LinkHeader: any; +declare const ajv: any; +declare const parse: any; +declare var async: any; +declare const ArgumentParser: any, argparser: any; +declare const md5: any; +declare const arg: { + integer(v: any): number; + file(v: any): any; + path(v: any): any; + json(v: any): any; +}; +declare function parArg(api: any): any; +declare function $key(argparser?: any): Promise; +declare function $get(argparser?: any): Promise; +declare function $post(argparser?: any): Promise; +declare function $put(argparser?: any): Promise; +declare function $delete(argparser?: any): Promise; +declare const ee: any; diff --git a/bin/zotero-cli.ts b/bin/zotero-cli.ts index a90f7ac..285c80b 100755 --- a/bin/zotero-cli.ts +++ b/bin/zotero-cli.ts @@ -10,7 +10,7 @@ const os = require('os'); const { Zotero } = require('./zotero-api-lib.ts'); -import { ArgumentParser } from 'argparse' +// import { ArgumentParser } from 'argparse' const TOML = require('@iarna/toml'); const fs = require('fs'); const path = require('path'); @@ -77,7 +77,30 @@ function parArg(api) { sp.addArgument('--key', { help: 'Show all the child collections of collection with key. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) sp.addArgument('--create-child', { nargs: '*', help: 'Create child collections of key (or at the top level if no key is specified) with the names specified.' }) } - + if (cmd === "$items") { + sp.addArgument('--count', { action: 'storeTrue', help: 'Return the number of items.' }) + // argparser.addArgument('--all', { action: 'storeTrue', help: 'obsolete' }) + sp.addArgument('--filter', { type: arg.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. For example: \'{"format": "json,bib", "limit": 100, "start": 100}\'.' }) + sp.addArgument('--collection', { help: 'Retrive list of items for collection. You can provide the collection key as a zotero-select link (zotero://...) to also set the group-id.' }) + sp.addArgument('--top', { action: 'storeTrue', help: 'Retrieve top-level items in the library/collection (excluding child items / attachments, excluding trashed items).' }) + sp.addArgument('--validate', { type: arg.path, help: 'json-schema file for all itemtypes, or directory with schema files, one per itemtype.' }) + } + if (cmd === "$item") { + sp.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + sp.addArgument('--children', { action: 'storeTrue', help: 'Retrieve list of children for the item.' }) + sp.addArgument('--filter', { type: arg.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. To retrieve multiple items you have use "itemkey"; for example: \'{"format": "json,bib", "itemkey": "A,B,C"}\'. See https://www.zotero.org/support/dev/web_api/v3/basics#search_syntax.' }) + sp.addArgument('--addfile', { nargs: '*', help: 'Upload attachments to the item. (/items/new)' }) + sp.addArgument('--savefiles', { nargs: '*', help: 'Download all attachments from the item (/items/KEY/file).' }) + sp.addArgument('--addtocollection', { nargs: '*', help: 'Add item to collections. (Convenience method: patch item->data->collections.)' }) + sp.addArgument('--removefromcollection', { nargs: '*', help: 'Remove item from collections. (Convenience method: patch item->data->collections.)' }) + sp.addArgument('--addtags', { nargs: '*', help: 'Add tags to item. (Convenience method: patch item->data->tags.)' }) + sp.addArgument('--removetags', { nargs: '*', help: 'Remove tags from item. (Convenience method: patch item->data->tags.)' }) + } + if (cmd === "$attachment") { + sp.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + sp.addArgument('--save', { required: true, help: 'Filename to save attachment to.' }) + } + } else { api[cmd](sp) @@ -104,7 +127,7 @@ async function $get(argparser = null) { /** Make a direct query to the API using 'GET uri'. */ console.log("rrrrrrr") - if (argparser) { + if (argparser) { argparser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) return @@ -123,6 +146,12 @@ argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + + + + + + for (const uri of this.args.uri) { From 65ffb71162c76b384eacd22c85d996317d0c9e89 Mon Sep 17 00:00:00 2001 From: reyamme <47262195+reyamme@users.noreply.github.com> Date: Thu, 7 Jan 2021 20:29:36 +0000 Subject: [PATCH 08/10] create and update-item --- bin/zotero-api-lib.ts | 22 +++++++++++----------- bin/zotero-cli.ts | 13 ++++++++++++- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/bin/zotero-api-lib.ts b/bin/zotero-api-lib.ts index aa5bf60..e88d579 100644 --- a/bin/zotero-api-lib.ts +++ b/bin/zotero-api-lib.ts @@ -700,11 +700,11 @@ export class Zotero { Use this option to create both top-level items, as well as child items (including notes and links). */ - if (argparser) { - argparser.addArgument('--template', { help: "Retrieve a template for the item you wish to create. You can retrieve the template types using the main argument 'types'." }) - argparser.addArgument('items', { nargs: '*', help: 'Json files for the items to be created.' }) - return - } + // if (argparser) { + // argparser.addArgument('--template', { help: "Retrieve a template for the item you wish to create. You can retrieve the template types using the main argument 'types'." }) + // argparser.addArgument('items', { nargs: '*', help: 'Json files for the items to be created.' }) + // return + // } if (this.args.template) { this.show(await this.get('/items/new', { userOrGroupPrefix: false, params: { itemType: this.args.template } })) @@ -720,12 +720,12 @@ export class Zotero { async $update_item(argparser = null) { /** Update/replace an item (--key KEY), either update (API: patch /items/KEY) or replacing (using --replace, API: put /items/KEY). */ - if (argparser) { - argparser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) - argparser.addArgument('--replace', { action: 'storeTrue', help: 'Replace the item by sumbitting the complete json.' }) - argparser.addArgument('items', { nargs: 1, help: 'Path of item files in json format.' }) - return - } + // if (argparser) { + // argparser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + // argparser.addArgument('--replace', { action: 'storeTrue', help: 'Replace the item by sumbitting the complete json.' }) + // argparser.addArgument('items', { nargs: 1, help: 'Path of item files in json format.' }) + // return + // } if (this.args.key) { this.args.key = this.extractKeyAndSetGroup(this.args.key) diff --git a/bin/zotero-cli.ts b/bin/zotero-cli.ts index 285c80b..04d6a53 100755 --- a/bin/zotero-cli.ts +++ b/bin/zotero-cli.ts @@ -100,6 +100,15 @@ function parArg(api) { sp.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) sp.addArgument('--save', { required: true, help: 'Filename to save attachment to.' }) } + if (cmd === "$create_item") { + sp.addArgument('--template', { help: "Retrieve a template for the item you wish to create. You can retrieve the template types using the main argument 'types'." }) + sp.addArgument('items', { nargs: '*', help: 'Json files for the items to be created.' }) + } + if (cmd === "$update_item") { + sp.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + sp.addArgument('--replace', { action: 'storeTrue', help: 'Replace the item by sumbitting the complete json.' }) + sp.addArgument('items', { nargs: 1, help: 'Path of item files in json format.' }) + } } else { @@ -127,7 +136,7 @@ async function $get(argparser = null) { /** Make a direct query to the API using 'GET uri'. */ console.log("rrrrrrr") - if (argparser) { + if (argparser) { argparser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) return @@ -150,6 +159,8 @@ argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + + From 918bd1a4895d0ca9838a0b712e5974563c9c6ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ha=C3=9Fler?= Date: Sun, 10 Jan 2021 23:53:11 +0000 Subject: [PATCH 09/10] zotzen-workspace (tidy up) --- BaCkUp/package.210104.0001.json | 60 ++++++ BaCkUp/package.210104.0002.json | 60 ++++++ BaCkUp/package.210104.json | 57 +++++ BaCkUp/package.210106.0001.json | 62 ++++++ BaCkUp/package.210106.json | 62 ++++++ bin/BaCkUp/zotero-cli.210106.0001.d.ts | 2 + bin/BaCkUp/zotero-cli.210106.0001.ts | 259 +++++++++++++++++++++++ bin/BaCkUp/zotero-cli.210106.0002.d.ts | 1 + bin/BaCkUp/zotero-cli.210106.0002.ts | 179 ++++++++++++++++ bin/BaCkUp/zotero-cli.210106.0003.d.ts | 2 + bin/BaCkUp/zotero-cli.210106.0003.ts | 263 +++++++++++++++++++++++ bin/BaCkUp/zotero-cli.210106.0004.d.ts | 2 + bin/BaCkUp/zotero-cli.210106.0004.ts | 263 +++++++++++++++++++++++ bin/BaCkUp/zotero-cli.210106.0005.d.ts | 2 + bin/BaCkUp/zotero-cli.210106.0005.ts | 264 +++++++++++++++++++++++ bin/BaCkUp/zotero-cli.210106.0006.d.ts | 2 + bin/BaCkUp/zotero-cli.210106.0006.ts | 280 +++++++++++++++++++++++++ bin/BaCkUp/zotero-cli.210106.d.ts | 2 + bin/BaCkUp/zotero-cli.210106.ts | 250 ++++++++++++++++++++++ bin/BaCkUp/zotero-cli.d.210106.d.ts | 1 + bin/BaCkUp/zotero-cli.d.210106.ts | 1 + bin/zotero-cli.js~ | 229 ++++++++++++++++++++ 22 files changed, 2303 insertions(+) create mode 100644 BaCkUp/package.210104.0001.json create mode 100644 BaCkUp/package.210104.0002.json create mode 100644 BaCkUp/package.210104.json create mode 100644 BaCkUp/package.210106.0001.json create mode 100644 BaCkUp/package.210106.json create mode 100644 bin/BaCkUp/zotero-cli.210106.0001.d.ts create mode 100755 bin/BaCkUp/zotero-cli.210106.0001.ts create mode 100644 bin/BaCkUp/zotero-cli.210106.0002.d.ts create mode 100755 bin/BaCkUp/zotero-cli.210106.0002.ts create mode 100644 bin/BaCkUp/zotero-cli.210106.0003.d.ts create mode 100755 bin/BaCkUp/zotero-cli.210106.0003.ts create mode 100644 bin/BaCkUp/zotero-cli.210106.0004.d.ts create mode 100755 bin/BaCkUp/zotero-cli.210106.0004.ts create mode 100644 bin/BaCkUp/zotero-cli.210106.0005.d.ts create mode 100755 bin/BaCkUp/zotero-cli.210106.0005.ts create mode 100644 bin/BaCkUp/zotero-cli.210106.0006.d.ts create mode 100755 bin/BaCkUp/zotero-cli.210106.0006.ts create mode 100644 bin/BaCkUp/zotero-cli.210106.d.ts create mode 100755 bin/BaCkUp/zotero-cli.210106.ts create mode 100644 bin/BaCkUp/zotero-cli.d.210106.d.ts create mode 100644 bin/BaCkUp/zotero-cli.d.210106.ts create mode 100755 bin/zotero-cli.js~ diff --git a/BaCkUp/package.210104.0001.json b/BaCkUp/package.210104.0001.json new file mode 100644 index 0000000..c6ffbef --- /dev/null +++ b/BaCkUp/package.210104.0001.json @@ -0,0 +1,60 @@ +{ + "name": "zotero-cli", + "version": "1.0.6", + "description": "Command line for interacting with Zotero libraries", + "main": "bin/zotero-cli.js", + "preferGlobal": true, + "bin": { + "zotero-cli": "./bin/zotero-cli.js" + }, + "scripts": { + "start": "ts-node bin/zotero-cli.ts", + "preversion": "npm test", + "postversion": "git push --follow-tags", + "test": "tslint -t stylish --project .", + "build": "tsc && chmod +x bin/*.js", + "prepublishOnly": "npm run build", + "go": "npm run build && npm version patch && npm publish", + "publish:patch": "npm run build; npm version patch; npm publish; git push --tags", + "publish:minor": "npm run build; npm version minor; npm publish; git push --tags", + "publish:major": "npm run build; npm version major; npm publish; git push --tags" + "fixed": "npm test && npm run build && git add bin && git commit -m \"fixes #$FIXED\" && git push" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/opendeved/zotero-cli.git" + }, + "keywords": [ + "zotero" + ], + "author": "opendeved", + "license": "ISC", + "bugs": { + "url": "https://github.com/opendeved/zotero-cli/issues" + }, + "homepage": "https://github.com/opendeved/zotero-cli#readme", + "devDependencies": { + "@types/node": "^13.11.1", + "ts-node": "^8.8.2", + "tslint": "^6.1.1", + "typescript": "^3.8.3" + }, + "dependencies": { + "@iarna/toml": "^2.2.3", + "ajv": "^6.12.0", + "argparse": "^1.0.10", + "docstring": "^1.1.0", + "dotenv": "^8.2.0", + "http-link-header": "^1.0.2", + "md5-file": "^5.0.0", + "request": "^2.88.2", + "request-promise": "^4.2.5" + }, + "files": [ + "package.json", + "bin/zotero-cli.d.ts", + "bin/zotero-cli.js", + "docs/COMMANDS.md", + "README.md" + ] +} diff --git a/BaCkUp/package.210104.0002.json b/BaCkUp/package.210104.0002.json new file mode 100644 index 0000000..306eade --- /dev/null +++ b/BaCkUp/package.210104.0002.json @@ -0,0 +1,60 @@ +{ + "name": "zotero-cli", + "version": "1.0.6", + "description": "Command line for interacting with Zotero libraries", + "main": "bin/zotero-cli.js", + "preferGlobal": true, + "bin": { + "zotero-cli": "./bin/zotero-cli.js" + }, + "scripts": { + "start": "ts-node bin/zotero-cli.ts", + "preversion": "npm test", + "postversion": "git push --follow-tags", + "test": "tslint -t stylish --project .", + "build": "tsc && chmod +x bin/*.js", + "prepublishOnly": "npm run build", + "go": "npm run build && npm version patch && npm publish", + "publish:patch": "npm run build; npm version patch; npm publish; git push --tags", + "publish:minor": "npm run build; npm version minor; npm publish; git push --tags", + "publish:major": "npm run build; npm version major; npm publish; git push --tags", + "fixed": "npm test && npm run build && git add bin && git commit -m \"fixes #$FIXED\" && git push" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/opendeved/zotero-cli.git" + }, + "keywords": [ + "zotero" + ], + "author": "opendeved", + "license": "ISC", + "bugs": { + "url": "https://github.com/opendeved/zotero-cli/issues" + }, + "homepage": "https://github.com/opendeved/zotero-cli#readme", + "devDependencies": { + "@types/node": "^13.11.1", + "ts-node": "^8.8.2", + "tslint": "^6.1.1", + "typescript": "^3.8.3" + }, + "dependencies": { + "@iarna/toml": "^2.2.3", + "ajv": "^6.12.0", + "argparse": "^1.0.10", + "docstring": "^1.1.0", + "dotenv": "^8.2.0", + "http-link-header": "^1.0.2", + "md5-file": "^5.0.0", + "request": "^2.88.2", + "request-promise": "^4.2.5" + }, + "files": [ + "package.json", + "bin/zotero-cli.d.ts", + "bin/zotero-cli.js", + "docs/COMMANDS.md", + "README.md" + ] +} diff --git a/BaCkUp/package.210104.json b/BaCkUp/package.210104.json new file mode 100644 index 0000000..e48cec3 --- /dev/null +++ b/BaCkUp/package.210104.json @@ -0,0 +1,57 @@ +{ + "name": "zotero-cli", + "version": "1.0.6", + "description": "Command line for interacting with Zotero libraries", + "main": "bin/zotero-cli.js", + "preferGlobal": true, + "bin": { + "zotero-cli": "./bin/zotero-cli.js" + }, + "scripts": { + "start": "ts-node bin/zotero-cli.ts", + "preversion": "npm test", + "postversion": "git push --follow-tags", + "test": "tslint -t stylish --project .", + "build": "tsc && chmod +x bin/*.js", + "prepublishOnly": "npm run build", + "go": "npm run build && npm version patch && npm publish", + "fixed": "npm test && npm run build && git add bin && git commit -m \"fixes #$FIXED\" && git push" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/opendeved/zotero-cli.git" + }, + "keywords": [ + "zotero" + ], + "author": "opendeved", + "license": "ISC", + "bugs": { + "url": "https://github.com/opendeved/zotero-cli/issues" + }, + "homepage": "https://github.com/opendeved/zotero-cli#readme", + "devDependencies": { + "@types/node": "^13.11.1", + "ts-node": "^8.8.2", + "tslint": "^6.1.1", + "typescript": "^3.8.3" + }, + "dependencies": { + "@iarna/toml": "^2.2.3", + "ajv": "^6.12.0", + "argparse": "^1.0.10", + "docstring": "^1.1.0", + "dotenv": "^8.2.0", + "http-link-header": "^1.0.2", + "md5-file": "^5.0.0", + "request": "^2.88.2", + "request-promise": "^4.2.5" + }, + "files": [ + "package.json", + "bin/zotero-cli.d.ts", + "bin/zotero-cli.js", + "docs/COMMANDS.md", + "README.md" + ] +} diff --git a/BaCkUp/package.210106.0001.json b/BaCkUp/package.210106.0001.json new file mode 100644 index 0000000..683d37f --- /dev/null +++ b/BaCkUp/package.210106.0001.json @@ -0,0 +1,62 @@ +{ + "name": "zotero-cli", + "version": "1.0.6", + "description": "Command line for interacting with Zotero libraries", + "main": "bin/zotero-cli.js", + "preferGlobal": true, + "bin": { + "zotero-cli": "./bin/zotero-cli.js" + }, + "scripts": { + "start": "ts-node bin/zotero-cli.ts", + "preversion": "npm test", + "postversion": "git push --follow-tags", + "test": "tslint -t stylish --project .", + "build": "tsc && chmod +x bin/*.js", + "prepublishOnly": "npm run build", + "go": "npm run build && npm version patch && npm publish", + "publish:patch": "npm run build; npm version patch; npm publish; git push --tags", + "publish:minor": "npm run build; npm version minor; npm publish; git push --tags", + "publish:major": "npm run build; npm version major; npm publish; git push --tags", + "fixed": "npm test && npm run build && git add bin && git commit -m \"fixes #$FIXED\" && git push" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/opendeved/zotero-cli.git" + }, + "keywords": [ + "zotero" + ], + "author": "opendeved", + "license": "ISC", + "bugs": { + "url": "https://github.com/opendeved/zotero-cli/issues" + }, + "homepage": "https://github.com/opendeved/zotero-cli#readme", + "devDependencies": { + "@types/node": "^13.11.1", + "ts-node": "^8.8.2", + "tslint": "^6.1.1", + "typescript": "^3.8.3", + "zotero-cli": "^1.0.1" + }, + "dependencies": { + "@iarna/toml": "^2.2.3", + "ajv": "^6.12.0", + "argparse": "^2.0.1", + "docstring": "^1.1.0", + "dotenv": "^8.2.0", + "http-link-header": "^1.0.2", + "md5-file": "^5.0.0", + "request": "^2.88.2", + "request-promise": "^4.2.5", + "zotero-lib": "^1.0.5" + }, + "files": [ + "package.json", + "bin/zotero-cli.d.ts", + "bin/zotero-cli.js", + "docs/COMMANDS.md", + "README.md" + ] +} diff --git a/BaCkUp/package.210106.json b/BaCkUp/package.210106.json new file mode 100644 index 0000000..683d37f --- /dev/null +++ b/BaCkUp/package.210106.json @@ -0,0 +1,62 @@ +{ + "name": "zotero-cli", + "version": "1.0.6", + "description": "Command line for interacting with Zotero libraries", + "main": "bin/zotero-cli.js", + "preferGlobal": true, + "bin": { + "zotero-cli": "./bin/zotero-cli.js" + }, + "scripts": { + "start": "ts-node bin/zotero-cli.ts", + "preversion": "npm test", + "postversion": "git push --follow-tags", + "test": "tslint -t stylish --project .", + "build": "tsc && chmod +x bin/*.js", + "prepublishOnly": "npm run build", + "go": "npm run build && npm version patch && npm publish", + "publish:patch": "npm run build; npm version patch; npm publish; git push --tags", + "publish:minor": "npm run build; npm version minor; npm publish; git push --tags", + "publish:major": "npm run build; npm version major; npm publish; git push --tags", + "fixed": "npm test && npm run build && git add bin && git commit -m \"fixes #$FIXED\" && git push" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/opendeved/zotero-cli.git" + }, + "keywords": [ + "zotero" + ], + "author": "opendeved", + "license": "ISC", + "bugs": { + "url": "https://github.com/opendeved/zotero-cli/issues" + }, + "homepage": "https://github.com/opendeved/zotero-cli#readme", + "devDependencies": { + "@types/node": "^13.11.1", + "ts-node": "^8.8.2", + "tslint": "^6.1.1", + "typescript": "^3.8.3", + "zotero-cli": "^1.0.1" + }, + "dependencies": { + "@iarna/toml": "^2.2.3", + "ajv": "^6.12.0", + "argparse": "^2.0.1", + "docstring": "^1.1.0", + "dotenv": "^8.2.0", + "http-link-header": "^1.0.2", + "md5-file": "^5.0.0", + "request": "^2.88.2", + "request-promise": "^4.2.5", + "zotero-lib": "^1.0.5" + }, + "files": [ + "package.json", + "bin/zotero-cli.d.ts", + "bin/zotero-cli.js", + "docs/COMMANDS.md", + "README.md" + ] +} diff --git a/bin/BaCkUp/zotero-cli.210106.0001.d.ts b/bin/BaCkUp/zotero-cli.210106.0001.d.ts new file mode 100644 index 0000000..b798801 --- /dev/null +++ b/bin/BaCkUp/zotero-cli.210106.0001.d.ts @@ -0,0 +1,2 @@ +#!/usr/bin/env node +export {}; diff --git a/bin/BaCkUp/zotero-cli.210106.0001.ts b/bin/BaCkUp/zotero-cli.210106.0001.ts new file mode 100755 index 0000000..529b913 --- /dev/null +++ b/bin/BaCkUp/zotero-cli.210106.0001.ts @@ -0,0 +1,259 @@ +#!/usr/bin/env node +import { parse } from '@iarna/toml'; +import * as argparse from 'argparse'; + +var Zotero = require('../zotero-lib/bin/zotero-api-lib'); + + +function getArguments() { + + const parser = new argparse.ArgumentParser({ "description": "Zotero command line utility" }); + parser.addArgument('--api-key', { help: 'The API key to access the Zotero API.' }) + parser.addArgument('--config', { type: argparse.file, help: 'Configuration file (toml format). Note that ./zotero-cli.toml and ~/.config/zotero-cli/zotero-cli.toml is picked up automatically.' }) + parser.addArgument('--user-id', { type: argparse.integer, help: 'The id of the user library.' }) + parser.addArgument('--group-id', { type: argparse.integer, help: 'The id of the group library.' }) + // See below. If changed, add: You can provide the group-id as zotero-select link (zotero://...). Only the group-id is used, the item/collection id is discarded. + parser.addArgument('--indent', { type: argparse.integer, help: 'Identation for json output.' }) + parser.addArgument('--out', { help: 'Output to file' }) + parser.addArgument('--verbose', { action: 'storeTrue', help: 'Log requests.' }) + +/* +The following code explcitly adds subparsers. +Previously these were defined by the functions themselves (see below). +*/ + + //async $collections + const subparsers = parser.add_subparsers({ "help": "sub-command help" }); + const parser_collections = subparsers.add_parser("collections", { "help": "Collections command" }); + parser_collections.addArgument('--top', { action: 'storeTrue', help: 'Show only collection at top level.' }) + parser_collections.addArgument('--key', { action:'',help: 'Show all the child collections of collection with key. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_collections.addArgument('--create-child', { nargs: '*', help: 'Create child collections of key (or at the top level if no key is specified) with the names specified.' }) + + //async $collection + const parser_collection = subparsers.add_parser("collection", { "help": "Collection command" }); + parser_collection.addArgument('--key', { required: true, help: 'The key of the collection (required). You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_collection.addArgument('--tags', { action: 'storeTrue', help: 'Display tags present in the collection.' }) + parser_collection.addArgument('itemkeys', { nargs: '*' , help: 'Item keys for items to be added or removed from this collection.'}) + parser_collection.addArgument('--add', { nargs: '*', help: 'Add items to this collection. Note that adding items to collections with \'item --addtocollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + parser_collection.addArgument('--remove', { nargs: '*', help: 'Convenience method: Remove items from this collection. Note that removing items from collections with \'item --removefromcollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + + + //async items + const parser_items = subparsers.add_parser("items", { "help": "Items command" }); + parser_items.addArgument('--count', { action: 'storeTrue', help: 'Return the number of items.' }) + // argparser.addArgument('--all', { action: 'storeTrue', help: 'obsolete' }) + parser_items.addArgument('--filter', { type: argparse.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. For example: \'{"format": "json,bib", "limit": 100, "start": 100}\'.' }) + parser_items.addArgument('--collection', { help: 'Retrive list of items for collection. You can provide the collection key as a zotero-select link (zotero://...) to also set the group-id.' }) + parser_items.addArgument('--top', { action: 'storeTrue', help: 'Retrieve top-level items in the library/collection (excluding child items / attachments, excluding trashed items).' }) + parser_items.addArgument('--validate', { type: argparse.path, help: 'json-schema file for all itemtypes, or directory with schema files, one per itemtype.' }) + + //async item + const parser_item = subparsers.add_parser("items", { "help": "Item command" }); + parser_item.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_item.addArgument('--children', { action: 'storeTrue', help: 'Retrieve list of children for the item.' }) + parser_item.addArgument('--filter', { type: argparse.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. To retrieve multiple items you have use "itemkey"; for example: \'{"format": "json,bib", "itemkey": "A,B,C"}\'. See https://www.zotero.org/support/dev/web_api/v3/basics#search_syntax.' }) + parser_item.addArgument('--addfile', { nargs: '*', help: 'Upload attachments to the item. (/items/new)' }) + parser_item.addArgument('--savefiles', { nargs: '*', help: 'Download all attachments from the item (/items/KEY/file).' }) + parser_item.addArgument('--addtocollection', { nargs: '*', help: 'Add item to collections. (Convenience method: patch item->data->collections.)' }) + parser_item.addArgument('--removefromcollection', { nargs: '*', help: 'Remove item from collections. (Convenience method: patch item->data->collections.)' }) + parser_item.addArgument('--addtags', { nargs: '*', help: 'Add tags to item. (Convenience method: patch item->data->tags.)' }) + parser_item.addArgument('--removetags', { nargs: '*', help: 'Remove tags from item. (Convenience method: patch item->data->tags.)' }) + + //async attachement + const parser_attachment = subparsers.add_parser("attachment", { "help": "Item command" }); + parser_attachment.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_attachment.addArgument('--save', { required: true, help: 'Filename to save attachment to.' }) + + //async create item + const parser_create = subparsers.add_parser("attachment", { "help": "Create command" }); + parser_create.addArgument('--template', { help: "Retrieve a template for the item you wish to create. You can retrieve the template types using the main argument 'types'." }) + parser_create.addArgument('items', { nargs: '*', help: 'Json files for the items to be created.' }) + + //update item + const parser_update = subparsers.add_parser("update", { "help": "update command" }); + parser_update.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_update.addArgument('--replace', { action: 'storeTrue', help: 'Replace the item by sumbitting the complete json.' }) + parser_update.addArgument('items', { nargs: 1, help: 'Path of item files in json format.' }) + + + //async get + const parser_get = subparsers.add_parser("get", { "help": "get command" }); + parser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) + parser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + + //async post + const parser_post = subparsers.add_parser("post", { "help": "post command" }); + parser_post.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + parser_post.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + + + //async put + const parser_put = subparsers.add_parser("put", { "help": "put command" }); + parser_put.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + parser_put.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + + //async delete + const parser_delete = subparsers.add_parser("delete", { "help": "delete command" }); + parser_delete.addArgument('uri', { nargs: '+', help: 'Request uri' }) + + + //fields + const parser_fields = subparsers.add_parser("delete", { "help": "fields command" }); + parser_fields.addArgument('--type', { help: 'Display fields types for TYPE.' }) + + //searches + const parser_searches = subparsers.add_parser("searches", { "help": "searches command" }); + parser_searches.addArgument('--create', { nargs: 1, help: 'Path of JSON file containing the definitions of saved searches.' }) + + //tags + const parser_tags = subparsers.add_parser("tags", { "help": "tags command" }); + parser_tags.addArgument('--filter', { help: 'Tags of all types matching a specific name.' }) + parser_tags.addArgument('--count', { action: 'storeTrue', help: 'TODO: document' }) + + /* + +This was the previous method of getting sub-parsers. + +It extracts the sub-parser functions from each function. This is a +smart way of defining the interface. + + */ + /* + const subparsers = parser.addSubparsers({ title: 'commands', dest: 'command', required: true }) + // add all methods that do not start with _ as a command + for (const cmd of Object.getOwnPropertyNames(Object.getPrototypeOf(parser)).sort()) { + if (typeof parser[cmd] !== 'function' || cmd[0] !== '$') continue + + const sp = subparsers.addParser(cmd.slice(1).replace(/_/g, '-'), { description: parser[cmd].__doc__, help: parser[cmd].__doc__ }) + // when called with an argparser, the command is expected to add relevant parameters and return + // the command must have a docstring + parser[cmd](sp) + } */ + + // Other URLs + // https://www.zotero.org/support/dev/web_api/v3/basics + // /keys/ + // /users//groups + /* + async function $key(argparse) { + /** Show details about this API key. (API: /keys ) */ +/* + this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) + }*/ + + + // Functions for get, post, put, patch, delete. (Delete query to API with uri.) + + /* + async function $get(argparser = null) { + /** Make a direct query to the API using 'GET uri'. */ +/* + if (argparser) { + argparser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) + argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + return + } + + for (const uri of this.args.uri) { + this.show(await this.get(uri, { userOrGroupPrefix: !this.args.root })) + } + } +*/ + + +/* + + async function $post(argparser = null) { + /** Make a direct query to the API using 'POST uri [--data data]'. */ +/* + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.post(this.args.uri, this.args.data)) + } +*/ + + +/* + async function $put(argparser = null) { + /** Make a direct query to the API using 'PUT uri [--data data]'. */ + /* + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.put(this.args.uri, this.args.data)) + } + + */ + + + /* + + async function $delete(argparser = null) { + /** Make a direct delete query to the API using 'DELETE uri'. */ +/* + if (argparser) { + argparser.addArgument('uri', { nargs: '+', help: 'Request uri' }) + return + } + + for (const uri of this.args.uri) { + const response = await this.get(uri) + await this.delete(uri, response.version) + } + } + + */ + + + //parser.set_defaults({ "func": new Zotero().run() }); + //this.parser.parse_args(); + + return parser.parseArgs(); + +} + + +// --- main --- +var args = getArguments() + +/* +if (args.verbose) { + console.log("zotero-cli starting...") +} +// zenodo-cli create --title "..." --authors "..." --dryrun +if (args.dryrun) { + console.log(`API command:\n Zotero.${args.func.name}(${JSON.stringify(args, null, 2)})`); +} else { + // ZenodoAPI.${args.func.name}(args) + args.func(args); +} +*/ + + + +//console.log(`Here...${zoterolib.Zotero}`); + +var test = new Zotero(args); +test.func(args).catch(err => { + console.error('error:', err) + process.exit(1) +}); + +/* +(zoterolib.zotero.run().catch(err => { + console.error('error:', err) + process.exit(1) +}) +) +*/ + +module.exports = { + node: 'current' +}; diff --git a/bin/BaCkUp/zotero-cli.210106.0002.d.ts b/bin/BaCkUp/zotero-cli.210106.0002.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/bin/BaCkUp/zotero-cli.210106.0002.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/bin/BaCkUp/zotero-cli.210106.0002.ts b/bin/BaCkUp/zotero-cli.210106.0002.ts new file mode 100755 index 0000000..a90f7ac --- /dev/null +++ b/bin/BaCkUp/zotero-cli.210106.0002.ts @@ -0,0 +1,179 @@ + +// Other URLs +// https://www.zotero.org/support/dev/web_api/v3/basics +// /keys/ +// /users//groups + +require('dotenv').config(); +require('docstring'); +const os = require('os'); + +const { Zotero } = require('./zotero-api-lib.ts'); + +import { ArgumentParser } from 'argparse' +const TOML = require('@iarna/toml'); +const fs = require('fs'); +const path = require('path'); +const request = require('request-promise'); +const { LinkHeader } = require('http-link-header'); +const ajv = require('ajv'); +const { parse } = require("args-any"); +var async = require("async"); +const { ArgumentParser, argparser } = require('argparse'); +const md5 = require('md5') + +const arg = new class { + integer(v) { + if (isNaN(parseInt(v))) throw new Error(`${JSON.stringify(v)} is not an integer`) + return parseInt(v) + } + file(v) { + if (!fs.existsSync(v) || !fs.lstatSync(v).isFile()) throw new Error(`${JSON.stringify(v)} is not a file`) + return v + } + + path(v) { + if (!fs.existsSync(v)) throw new Error(`${JSON.stringify(v)} does not exist`) + return v + } + + json(v) { + return JSON.parse(v) + } +} + + +function parArg(api) { + + let parser = new ArgumentParser + // let argparser = new ArgumentParser + parser.addArgument('--api-key', { help: 'The API key to access the Zotero API.' }) + parser.addArgument('--config', { type: arg.file, help: 'Configuration file (toml format). Note that ./zotero-cli.toml and ~/.config/zotero-cli/zotero-cli.toml is picked up automatically.' }) + parser.addArgument('--user-id', { type: arg.integer, help: 'The id of the user library.' }) + parser.addArgument('--group-id', { type: arg.integer, help: 'The id of the group library.' }) + // See below. If changed, add: You can provide the group-id as zotero-select link (zotero://...). Only the group-id is used, the item/collection id is discarded. + parser.addArgument('--indent', { type: arg.integer, help: 'Identation for json output.' }) + parser.addArgument('--out', { help: 'Output to file' }) + parser.addArgument('--verbose', { action: 'storeTrue', help: 'Log requests.' }) + + const subparsers = parser.addSubparsers({ title: 'commands', dest: 'command', required: true }) + // add all methods that do not start with _ as a command + for (const cmd of Object.getOwnPropertyNames(Object.getPrototypeOf(api)).sort()) { + if (typeof api[cmd] !== 'function' || cmd[0] !== '$') continue + + const sp = subparsers.addParser(cmd.slice(1).replace(/_/g, '-'), { description: api[cmd].__doc__, help: api[cmd].__doc__ }) + // when called with an argparser, the command is expected to add relevant parameters and return + // the command must have a docstring + if (sp) { + if (cmd === "$collection") { + sp.addArgument('--key', { required: true, help: 'The key of the collection (required). You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + sp.addArgument('--tags', { action: 'storeTrue', help: 'Display tags present in the collection.' }) + // argparser.addArgument('itemkeys', { nargs: '*' , help: 'Item keys for items to be added or removed from this collection.'}) + sp.addArgument('--add', { nargs: '*', help: 'Add items to this collection. Note that adding items to collections with \'item --addtocollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + sp.addArgument('--remove', { nargs: '*', help: 'Convenience method: Remove items from this collection. Note that removing items from collections with \'item --removefromcollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + } + if (cmd === "$collections") { + sp.addArgument('--top', { action: 'storeTrue', help: 'Show only collection at top level.' }) + sp.addArgument('--key', { help: 'Show all the child collections of collection with key. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + sp.addArgument('--create-child', { nargs: '*', help: 'Create child collections of key (or at the top level if no key is specified) with the names specified.' }) + } + + } + else { + api[cmd](sp) + } + } + + return parser +} + + + + +async function $key(argparser = null) { +/** Show details about this API key. (API: /keys ) */ + + if (argparser) return + + this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) +} + +// Functions for get, post, put, patch, delete. (Delete query to API with uri.) + +async function $get(argparser = null) { + /** Make a direct query to the API using 'GET uri'. */ +console.log("rrrrrrr") + + if (argparser) { + argparser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) +argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + return +} + + + + + + + + + + + + + + + + + for (const uri of this.args.uri) { + this.show(await this.get(uri, { userOrGroupPrefix: !this.args.root })) + } +} + +async function $post(argparser = null) { + /** Make a direct query to the API using 'POST uri [--data data]'. */ + + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.post(this.args.uri, this.args.data)) +} + +async function $put(argparser = null) { + /** Make a direct query to the API using 'PUT uri [--data data]'. */ + + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.put(this.args.uri, this.args.data)) +}; + +async function $delete(argparser = null) { + /** Make a direct delete query to the API using 'DELETE uri'. */ + + if (argparser) { + argparser.addArgument('uri', { nargs: '+', help: 'Request uri' }) + return + } + + for (const uri of this.args.uri) { + const response = await this.get(uri) + await this.delete(uri, response.version) + } +} +const ee = new Zotero() +ee.arg = arg +ee.parser = parArg(ee) +ee.args = parArg(ee).parseArgs() +ee.run( + + ).catch(err => { + console.error('error:', err) + process.exit(1) +}) diff --git a/bin/BaCkUp/zotero-cli.210106.0003.d.ts b/bin/BaCkUp/zotero-cli.210106.0003.d.ts new file mode 100644 index 0000000..b798801 --- /dev/null +++ b/bin/BaCkUp/zotero-cli.210106.0003.d.ts @@ -0,0 +1,2 @@ +#!/usr/bin/env node +export {}; diff --git a/bin/BaCkUp/zotero-cli.210106.0003.ts b/bin/BaCkUp/zotero-cli.210106.0003.ts new file mode 100755 index 0000000..db1fa47 --- /dev/null +++ b/bin/BaCkUp/zotero-cli.210106.0003.ts @@ -0,0 +1,263 @@ +#!/usr/bin/env node +import { parse } from '@iarna/toml'; +import * as argparse from 'argparse'; + +var Zotero = require('../zotero-lib/bin/zotero-api-lib'); + + +function getArguments() { + + const parser = new argparse.ArgumentParser({ "description": "Zotero command line utility" }); + parser.addArgument('--api-key', { help: 'The API key to access the Zotero API.' }) + parser.addArgument('--config', { type: argparse.file, help: 'Configuration file (toml format). Note that ./zotero-cli.toml and ~/.config/zotero-cli/zotero-cli.toml is picked up automatically.' }) + parser.addArgument('--user-id', { type: argparse.integer, help: 'The id of the user library.' }) + parser.addArgument('--group-id', { type: argparse.integer, help: 'The id of the group library.' }) + // See below. If changed, add: You can provide the group-id as zotero-select link (zotero://...). Only the group-id is used, the item/collection id is discarded. + parser.addArgument('--indent', { type: argparse.integer, help: 'Identation for json output.' }) + parser.addArgument('--out', { help: 'Output to file' }) + parser.addArgument('--verbose', { action: 'storeTrue', help: 'Log requests.' }) + +/* +The following code explcitly adds subparsers. +Previously these were defined by the functions themselves (see below). +*/ + + //async $collections + const subparsers = parser.add_subparsers({ "help": "sub-command help" }); + const parser_collections = subparsers.add_parser("collections", { "help": "Collections command" }); + parser_collections.addArgument('--top', { action: 'storeTrue', help: 'Show only collection at top level.' }) + parser_collections.addArgument('--key', { action:'store', required: true, help: 'Show all the child collections of collection with key. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_collections.addArgument('--create-child', { nargs: '*', help: 'Create child collections of key (or at the top level if no key is specified) with the names specified.' }) + + //async $collection + const parser_collection = subparsers.add_parser("collection", { "help": "Collection command" }); + parser_collection.addArgument('--key', { required: true, help: 'The key of the collection (required). You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_collection.addArgument('--tags', { action: 'storeTrue', help: 'Display tags present in the collection.' }) + parser_collection.addArgument('itemkeys', { nargs: '*' , help: 'Item keys for items to be added or removed from this collection.'}) + parser_collection.addArgument('--add', { nargs: '*', help: 'Add items to this collection. Note that adding items to collections with \'item --addtocollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + parser_collection.addArgument('--remove', { nargs: '*', help: 'Convenience method: Remove items from this collection. Note that removing items from collections with \'item --removefromcollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + + + //async items + const parser_items = subparsers.add_parser("items", { "help": "Items command" }); + parser_items.addArgument('--count', { action: 'storeTrue', help: 'Return the number of items.' }) + // argparser.addArgument('--all', { action: 'storeTrue', help: 'obsolete' }) + parser_items.addArgument('--filter', { type: argparse.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. For example: \'{"format": "json,bib", "limit": 100, "start": 100}\'.' }) + parser_items.addArgument('--collection', { help: 'Retrive list of items for collection. You can provide the collection key as a zotero-select link (zotero://...) to also set the group-id.' }) + parser_items.addArgument('--top', { action: 'storeTrue', help: 'Retrieve top-level items in the library/collection (excluding child items / attachments, excluding trashed items).' }) + parser_items.addArgument('--validate', { type: argparse.path, help: 'json-schema file for all itemtypes, or directory with schema files, one per itemtype.' }) + + //async item + const parser_item = subparsers.add_parser("item", { "help": "Item command" }); + parser_item.addArgument('--key', { required: true, nargs: 1, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_item.addArgument('--children', { action: 'storeTrue', help: 'Retrieve list of children for the item.' }) + parser_item.addArgument('--filter', { type: argparse.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. To retrieve multiple items you have use "itemkey"; for example: \'{"format": "json,bib", "itemkey": "A,B,C"}\'. See https://www.zotero.org/support/dev/web_api/v3/basics#search_syntax.' }) + parser_item.addArgument('--addfile', { nargs: '*', help: 'Upload attachments to the item. (/items/new)' }) + parser_item.addArgument('--savefiles', { nargs: '*', help: 'Download all attachments from the item (/items/KEY/file).' }) + parser_item.addArgument('--addtocollection', { nargs: '*', help: 'Add item to collections. (Convenience method: patch item->data->collections.)' }) + parser_item.addArgument('--removefromcollection', { nargs: '*', help: 'Remove item from collections. (Convenience method: patch item->data->collections.)' }) + parser_item.addArgument('--addtags', { nargs: '*', help: 'Add tags to item. (Convenience method: patch item->data->tags.)' }) + parser_item.addArgument('--removetags', { nargs: '*', help: 'Remove tags from item. (Convenience method: patch item->data->tags.)' }) + + //async attachement + const parser_attachment = subparsers.add_parser("attachment", { "help": "Item command" }); + parser_attachment.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_attachment.addArgument('--save', { required: true, help: 'Filename to save attachment to.' }) + + //async create item + const parser_create = subparsers.add_parser("attachment", { "help": "Create command" }); + parser_create.addArgument('--template', { help: "Retrieve a template for the item you wish to create. You can retrieve the template types using the main argument 'types'." }) + parser_create.addArgument('items', { nargs: '*', help: 'Json files for the items to be created.' }) + + //update item + const parser_update = subparsers.add_parser("update", { "help": "update command" }); + parser_update.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_update.addArgument('--replace', { action: 'storeTrue', help: 'Replace the item by sumbitting the complete json.' }) + parser_update.addArgument('items', { nargs: 1, help: 'Path of item files in json format.' }) + + //async get + const parser_get = subparsers.add_parser("get", { "help": "get command" }); + parser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) + parser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + + //async post + const parser_post = subparsers.add_parser("post", { "help": "post command" }); + parser_post.addArgument('uri', { action: "store", nargs: 1, help: 'TODO: document' }) + parser_post.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + + //async put + const parser_put = subparsers.add_parser("put", { "help": "put command" }); + parser_put.addArgument('uri', { action: "store", nargs: 1, help: 'TODO: document' }) + parser_put.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + + //async delete + const parser_delete = subparsers.add_parser("delete", { "help": "delete command" }); + parser_delete.addArgument('uri', { nargs: '+', help: 'Request uri' }) + + //fields + const parser_fields = subparsers.add_parser("fields", { "help": "fields command" }); + parser_fields.addArgument('--type', { help: 'Display fields types for TYPE.' }) + + //searches + const parser_searches = subparsers.add_parser("searches", { "help": "searches command" }); + parser_searches.addArgument('--create', { nargs: 1, help: 'Path of JSON file containing the definitions of saved searches.' }) + + //tags + const parser_tags = subparsers.add_parser("tags", { "help": "tags command" }); + parser_tags.addArgument('--filter', { help: 'Tags of all types matching a specific name.' }) + parser_tags.addArgument('--count', { action: 'storeTrue', help: 'TODO: document' }) + + // TODO: reinstate key + /* + async function $key(argparse) { + //** + this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) + } + */ + + /* + + This was the previous method of getting sub-parsers. + + It extracts the sub-parser functions from each function. This is a + smart way of defining the interface. + + */ + /* + const subparsers = parser.addSubparsers({ title: 'commands', dest: 'command', required: true }) + // add all methods that do not start with _ as a command + for (const cmd of Object.getOwnPropertyNames(Object.getPrototypeOf(parser)).sort()) { + if (typeof parser[cmd] !== 'function' || cmd[0] !== '$') continue + const sp = subparsers.addParser(cmd.slice(1).replace(/_/g, '-'), { description: parser[cmd].__doc__, help: parser[cmd].__doc__ }) + // when called with an argparser, the command is expected to add relevant parameters and return + // the command must have a docstring + parser[cmd](sp) + } */ + + // Other URLs + // https://www.zotero.org/support/dev/web_api/v3/basics + // /keys/ + // /users//groups + /* + async function $key(argparse) { + /** Show details about this API key. (API: /keys ) */ +/* + this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) + }*/ + + + // Functions for get, post, put, patch, delete. (Delete query to API with uri.) + + /* + async function $get(argparser = null) { + /** Make a direct query to the API using 'GET uri'. */ +/* + if (argparser) { + argparser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) + argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + return + } + + for (const uri of this.args.uri) { + this.show(await this.get(uri, { userOrGroupPrefix: !this.args.root })) + } + } +*/ + + +/* + + async function $post(argparser = null) { + /** Make a direct query to the API using 'POST uri [--data data]'. */ +/* + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.post(this.args.uri, this.args.data)) + } +*/ + + +/* + async function $put(argparser = null) { + /** Make a direct query to the API using 'PUT uri [--data data]'. */ + /* + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.put(this.args.uri, this.args.data)) + } + + */ + + + /* + + async function $delete(argparser = null) { + /** Make a direct delete query to the API using 'DELETE uri'. */ +/* + if (argparser) { + argparser.addArgument('uri', { nargs: '+', help: 'Request uri' }) + return + } + + for (const uri of this.args.uri) { + const response = await this.get(uri) + await this.delete(uri, response.version) + } + } + + */ + + + //parser.set_defaults({ "func": new Zotero().run() }); + //this.parser.parse_args(); + + return parser.parseArgs(); + +} + + +// --- main --- +var args = getArguments() + +/* +if (args.verbose) { + console.log("zotero-cli starting...") +} +// zenodo-cli create --title "..." --authors "..." --dryrun +if (args.dryrun) { + console.log(`API command:\n Zotero.${args.func.name}(${JSON.stringify(args, null, 2)})`); +} else { + // ZenodoAPI.${args.func.name}(args) + args.func(args); +} +*/ + + + +//console.log(`Here...${zoterolib.Zotero}`); + +var test = new Zotero(args); +test.func(args).catch(err => { + console.error('error:', err) + process.exit(1) +}); + +/* +(zoterolib.zotero.run().catch(err => { + console.error('error:', err) + process.exit(1) +}) +) +*/ + +module.exports = { + node: 'current' +}; diff --git a/bin/BaCkUp/zotero-cli.210106.0004.d.ts b/bin/BaCkUp/zotero-cli.210106.0004.d.ts new file mode 100644 index 0000000..b798801 --- /dev/null +++ b/bin/BaCkUp/zotero-cli.210106.0004.d.ts @@ -0,0 +1,2 @@ +#!/usr/bin/env node +export {}; diff --git a/bin/BaCkUp/zotero-cli.210106.0004.ts b/bin/BaCkUp/zotero-cli.210106.0004.ts new file mode 100755 index 0000000..db1fa47 --- /dev/null +++ b/bin/BaCkUp/zotero-cli.210106.0004.ts @@ -0,0 +1,263 @@ +#!/usr/bin/env node +import { parse } from '@iarna/toml'; +import * as argparse from 'argparse'; + +var Zotero = require('../zotero-lib/bin/zotero-api-lib'); + + +function getArguments() { + + const parser = new argparse.ArgumentParser({ "description": "Zotero command line utility" }); + parser.addArgument('--api-key', { help: 'The API key to access the Zotero API.' }) + parser.addArgument('--config', { type: argparse.file, help: 'Configuration file (toml format). Note that ./zotero-cli.toml and ~/.config/zotero-cli/zotero-cli.toml is picked up automatically.' }) + parser.addArgument('--user-id', { type: argparse.integer, help: 'The id of the user library.' }) + parser.addArgument('--group-id', { type: argparse.integer, help: 'The id of the group library.' }) + // See below. If changed, add: You can provide the group-id as zotero-select link (zotero://...). Only the group-id is used, the item/collection id is discarded. + parser.addArgument('--indent', { type: argparse.integer, help: 'Identation for json output.' }) + parser.addArgument('--out', { help: 'Output to file' }) + parser.addArgument('--verbose', { action: 'storeTrue', help: 'Log requests.' }) + +/* +The following code explcitly adds subparsers. +Previously these were defined by the functions themselves (see below). +*/ + + //async $collections + const subparsers = parser.add_subparsers({ "help": "sub-command help" }); + const parser_collections = subparsers.add_parser("collections", { "help": "Collections command" }); + parser_collections.addArgument('--top', { action: 'storeTrue', help: 'Show only collection at top level.' }) + parser_collections.addArgument('--key', { action:'store', required: true, help: 'Show all the child collections of collection with key. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_collections.addArgument('--create-child', { nargs: '*', help: 'Create child collections of key (or at the top level if no key is specified) with the names specified.' }) + + //async $collection + const parser_collection = subparsers.add_parser("collection", { "help": "Collection command" }); + parser_collection.addArgument('--key', { required: true, help: 'The key of the collection (required). You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_collection.addArgument('--tags', { action: 'storeTrue', help: 'Display tags present in the collection.' }) + parser_collection.addArgument('itemkeys', { nargs: '*' , help: 'Item keys for items to be added or removed from this collection.'}) + parser_collection.addArgument('--add', { nargs: '*', help: 'Add items to this collection. Note that adding items to collections with \'item --addtocollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + parser_collection.addArgument('--remove', { nargs: '*', help: 'Convenience method: Remove items from this collection. Note that removing items from collections with \'item --removefromcollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + + + //async items + const parser_items = subparsers.add_parser("items", { "help": "Items command" }); + parser_items.addArgument('--count', { action: 'storeTrue', help: 'Return the number of items.' }) + // argparser.addArgument('--all', { action: 'storeTrue', help: 'obsolete' }) + parser_items.addArgument('--filter', { type: argparse.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. For example: \'{"format": "json,bib", "limit": 100, "start": 100}\'.' }) + parser_items.addArgument('--collection', { help: 'Retrive list of items for collection. You can provide the collection key as a zotero-select link (zotero://...) to also set the group-id.' }) + parser_items.addArgument('--top', { action: 'storeTrue', help: 'Retrieve top-level items in the library/collection (excluding child items / attachments, excluding trashed items).' }) + parser_items.addArgument('--validate', { type: argparse.path, help: 'json-schema file for all itemtypes, or directory with schema files, one per itemtype.' }) + + //async item + const parser_item = subparsers.add_parser("item", { "help": "Item command" }); + parser_item.addArgument('--key', { required: true, nargs: 1, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_item.addArgument('--children', { action: 'storeTrue', help: 'Retrieve list of children for the item.' }) + parser_item.addArgument('--filter', { type: argparse.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. To retrieve multiple items you have use "itemkey"; for example: \'{"format": "json,bib", "itemkey": "A,B,C"}\'. See https://www.zotero.org/support/dev/web_api/v3/basics#search_syntax.' }) + parser_item.addArgument('--addfile', { nargs: '*', help: 'Upload attachments to the item. (/items/new)' }) + parser_item.addArgument('--savefiles', { nargs: '*', help: 'Download all attachments from the item (/items/KEY/file).' }) + parser_item.addArgument('--addtocollection', { nargs: '*', help: 'Add item to collections. (Convenience method: patch item->data->collections.)' }) + parser_item.addArgument('--removefromcollection', { nargs: '*', help: 'Remove item from collections. (Convenience method: patch item->data->collections.)' }) + parser_item.addArgument('--addtags', { nargs: '*', help: 'Add tags to item. (Convenience method: patch item->data->tags.)' }) + parser_item.addArgument('--removetags', { nargs: '*', help: 'Remove tags from item. (Convenience method: patch item->data->tags.)' }) + + //async attachement + const parser_attachment = subparsers.add_parser("attachment", { "help": "Item command" }); + parser_attachment.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_attachment.addArgument('--save', { required: true, help: 'Filename to save attachment to.' }) + + //async create item + const parser_create = subparsers.add_parser("attachment", { "help": "Create command" }); + parser_create.addArgument('--template', { help: "Retrieve a template for the item you wish to create. You can retrieve the template types using the main argument 'types'." }) + parser_create.addArgument('items', { nargs: '*', help: 'Json files for the items to be created.' }) + + //update item + const parser_update = subparsers.add_parser("update", { "help": "update command" }); + parser_update.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_update.addArgument('--replace', { action: 'storeTrue', help: 'Replace the item by sumbitting the complete json.' }) + parser_update.addArgument('items', { nargs: 1, help: 'Path of item files in json format.' }) + + //async get + const parser_get = subparsers.add_parser("get", { "help": "get command" }); + parser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) + parser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + + //async post + const parser_post = subparsers.add_parser("post", { "help": "post command" }); + parser_post.addArgument('uri', { action: "store", nargs: 1, help: 'TODO: document' }) + parser_post.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + + //async put + const parser_put = subparsers.add_parser("put", { "help": "put command" }); + parser_put.addArgument('uri', { action: "store", nargs: 1, help: 'TODO: document' }) + parser_put.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + + //async delete + const parser_delete = subparsers.add_parser("delete", { "help": "delete command" }); + parser_delete.addArgument('uri', { nargs: '+', help: 'Request uri' }) + + //fields + const parser_fields = subparsers.add_parser("fields", { "help": "fields command" }); + parser_fields.addArgument('--type', { help: 'Display fields types for TYPE.' }) + + //searches + const parser_searches = subparsers.add_parser("searches", { "help": "searches command" }); + parser_searches.addArgument('--create', { nargs: 1, help: 'Path of JSON file containing the definitions of saved searches.' }) + + //tags + const parser_tags = subparsers.add_parser("tags", { "help": "tags command" }); + parser_tags.addArgument('--filter', { help: 'Tags of all types matching a specific name.' }) + parser_tags.addArgument('--count', { action: 'storeTrue', help: 'TODO: document' }) + + // TODO: reinstate key + /* + async function $key(argparse) { + //** + this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) + } + */ + + /* + + This was the previous method of getting sub-parsers. + + It extracts the sub-parser functions from each function. This is a + smart way of defining the interface. + + */ + /* + const subparsers = parser.addSubparsers({ title: 'commands', dest: 'command', required: true }) + // add all methods that do not start with _ as a command + for (const cmd of Object.getOwnPropertyNames(Object.getPrototypeOf(parser)).sort()) { + if (typeof parser[cmd] !== 'function' || cmd[0] !== '$') continue + const sp = subparsers.addParser(cmd.slice(1).replace(/_/g, '-'), { description: parser[cmd].__doc__, help: parser[cmd].__doc__ }) + // when called with an argparser, the command is expected to add relevant parameters and return + // the command must have a docstring + parser[cmd](sp) + } */ + + // Other URLs + // https://www.zotero.org/support/dev/web_api/v3/basics + // /keys/ + // /users//groups + /* + async function $key(argparse) { + /** Show details about this API key. (API: /keys ) */ +/* + this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) + }*/ + + + // Functions for get, post, put, patch, delete. (Delete query to API with uri.) + + /* + async function $get(argparser = null) { + /** Make a direct query to the API using 'GET uri'. */ +/* + if (argparser) { + argparser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) + argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + return + } + + for (const uri of this.args.uri) { + this.show(await this.get(uri, { userOrGroupPrefix: !this.args.root })) + } + } +*/ + + +/* + + async function $post(argparser = null) { + /** Make a direct query to the API using 'POST uri [--data data]'. */ +/* + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.post(this.args.uri, this.args.data)) + } +*/ + + +/* + async function $put(argparser = null) { + /** Make a direct query to the API using 'PUT uri [--data data]'. */ + /* + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.put(this.args.uri, this.args.data)) + } + + */ + + + /* + + async function $delete(argparser = null) { + /** Make a direct delete query to the API using 'DELETE uri'. */ +/* + if (argparser) { + argparser.addArgument('uri', { nargs: '+', help: 'Request uri' }) + return + } + + for (const uri of this.args.uri) { + const response = await this.get(uri) + await this.delete(uri, response.version) + } + } + + */ + + + //parser.set_defaults({ "func": new Zotero().run() }); + //this.parser.parse_args(); + + return parser.parseArgs(); + +} + + +// --- main --- +var args = getArguments() + +/* +if (args.verbose) { + console.log("zotero-cli starting...") +} +// zenodo-cli create --title "..." --authors "..." --dryrun +if (args.dryrun) { + console.log(`API command:\n Zotero.${args.func.name}(${JSON.stringify(args, null, 2)})`); +} else { + // ZenodoAPI.${args.func.name}(args) + args.func(args); +} +*/ + + + +//console.log(`Here...${zoterolib.Zotero}`); + +var test = new Zotero(args); +test.func(args).catch(err => { + console.error('error:', err) + process.exit(1) +}); + +/* +(zoterolib.zotero.run().catch(err => { + console.error('error:', err) + process.exit(1) +}) +) +*/ + +module.exports = { + node: 'current' +}; diff --git a/bin/BaCkUp/zotero-cli.210106.0005.d.ts b/bin/BaCkUp/zotero-cli.210106.0005.d.ts new file mode 100644 index 0000000..b798801 --- /dev/null +++ b/bin/BaCkUp/zotero-cli.210106.0005.d.ts @@ -0,0 +1,2 @@ +#!/usr/bin/env node +export {}; diff --git a/bin/BaCkUp/zotero-cli.210106.0005.ts b/bin/BaCkUp/zotero-cli.210106.0005.ts new file mode 100755 index 0000000..94d589b --- /dev/null +++ b/bin/BaCkUp/zotero-cli.210106.0005.ts @@ -0,0 +1,264 @@ +#!/usr/bin/env node + +import { parse } from '@iarna/toml'; +import * as argparse from 'argparse'; + +var Zotero = require('../zotero-lib/bin/zotero-api-lib'); + + +function getArguments() { + + const parser = new argparse.ArgumentParser({ "description": "Zotero command line utility" }); + parser.addArgument('--api-key', { help: 'The API key to access the Zotero API.' }) + parser.addArgument('--config', { type: argparse.file, help: 'Configuration file (toml format). Note that ./zotero-cli.toml and ~/.config/zotero-cli/zotero-cli.toml is picked up automatically.' }) + parser.addArgument('--user-id', { type: argparse.integer, help: 'The id of the user library.' }) + parser.addArgument('--group-id', { type: argparse.integer, help: 'The id of the group library.' }) + // See below. If changed, add: You can provide the group-id as zotero-select link (zotero://...). Only the group-id is used, the item/collection id is discarded. + parser.addArgument('--indent', { type: argparse.integer, help: 'Identation for json output.' }) + parser.addArgument('--out', { help: 'Output to file' }) + parser.addArgument('--verbose', { action: 'storeTrue', help: 'Log requests.' }) + +/* +The following code explcitly adds subparsers. +Previously these were defined by the functions themselves (see below). +*/ + + //async $collections + const subparsers = parser.add_subparsers({ "help": "sub-command help" }); + const parser_collections = subparsers.add_parser("collections", { "help": "Collections command" }); + parser_collections.addArgument('--top', { action: 'storeTrue', help: 'Show only collection at top level.' }) + parser_collections.addArgument('--key', { action:'store', required: true, help: 'Show all the child collections of collection with key. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_collections.addArgument('--create-child', { nargs: '*', help: 'Create child collections of key (or at the top level if no key is specified) with the names specified.' }) + + //async $collection + const parser_collection = subparsers.add_parser("collection", { "help": "Collection command" }); + parser_collection.addArgument('--key', { required: true, help: 'The key of the collection (required). You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_collection.addArgument('--tags', { action: 'storeTrue', help: 'Display tags present in the collection.' }) + parser_collection.addArgument('itemkeys', { nargs: '*' , help: 'Item keys for items to be added or removed from this collection.'}) + parser_collection.addArgument('--add', { nargs: '*', help: 'Add items to this collection. Note that adding items to collections with \'item --addtocollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + parser_collection.addArgument('--remove', { nargs: '*', help: 'Convenience method: Remove items from this collection. Note that removing items from collections with \'item --removefromcollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + + + //async items + const parser_items = subparsers.add_parser("items", { "help": "Items command" }); + parser_items.addArgument('--count', { action: 'storeTrue', help: 'Return the number of items.' }) + // argparser.addArgument('--all', { action: 'storeTrue', help: 'obsolete' }) + parser_items.addArgument('--filter', { type: argparse.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. For example: \'{"format": "json,bib", "limit": 100, "start": 100}\'.' }) + parser_items.addArgument('--collection', { help: 'Retrive list of items for collection. You can provide the collection key as a zotero-select link (zotero://...) to also set the group-id.' }) + parser_items.addArgument('--top', { action: 'storeTrue', help: 'Retrieve top-level items in the library/collection (excluding child items / attachments, excluding trashed items).' }) + parser_items.addArgument('--validate', { type: argparse.path, help: 'json-schema file for all itemtypes, or directory with schema files, one per itemtype.' }) + + //async item + const parser_item = subparsers.add_parser("item", { "help": "Item command" }); + parser_item.addArgument('--key', { required: true, nargs: 1, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_item.addArgument('--children', { action: 'storeTrue', help: 'Retrieve list of children for the item.' }) + parser_item.addArgument('--filter', { type: argparse.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. To retrieve multiple items you have use "itemkey"; for example: \'{"format": "json,bib", "itemkey": "A,B,C"}\'. See https://www.zotero.org/support/dev/web_api/v3/basics#search_syntax.' }) + parser_item.addArgument('--addfile', { nargs: '*', help: 'Upload attachments to the item. (/items/new)' }) + parser_item.addArgument('--savefiles', { nargs: '*', help: 'Download all attachments from the item (/items/KEY/file).' }) + parser_item.addArgument('--addtocollection', { nargs: '*', help: 'Add item to collections. (Convenience method: patch item->data->collections.)' }) + parser_item.addArgument('--removefromcollection', { nargs: '*', help: 'Remove item from collections. (Convenience method: patch item->data->collections.)' }) + parser_item.addArgument('--addtags', { nargs: '*', help: 'Add tags to item. (Convenience method: patch item->data->tags.)' }) + parser_item.addArgument('--removetags', { nargs: '*', help: 'Remove tags from item. (Convenience method: patch item->data->tags.)' }) + + //async attachement + const parser_attachment = subparsers.add_parser("attachment", { "help": "Item command" }); + parser_attachment.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_attachment.addArgument('--save', { required: true, help: 'Filename to save attachment to.' }) + + //async create item + const parser_create = subparsers.add_parser("attachment", { "help": "Create command" }); + parser_create.addArgument('--template', { help: "Retrieve a template for the item you wish to create. You can retrieve the template types using the main argument 'types'." }) + parser_create.addArgument('items', { nargs: '*', help: 'Json files for the items to be created.' }) + + //update item + const parser_update = subparsers.add_parser("update", { "help": "update command" }); + parser_update.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_update.addArgument('--replace', { action: 'storeTrue', help: 'Replace the item by sumbitting the complete json.' }) + parser_update.addArgument('items', { nargs: 1, help: 'Path of item files in json format.' }) + + //async get + const parser_get = subparsers.add_parser("get", { "help": "get command" }); + parser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) + parser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + + //async post + const parser_post = subparsers.add_parser("post", { "help": "post command" }); + parser_post.addArgument('uri', { action: "store", nargs: 1, help: 'TODO: document' }) + parser_post.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + + //async put + const parser_put = subparsers.add_parser("put", { "help": "put command" }); + parser_put.addArgument('uri', { action: "store", nargs: 1, help: 'TODO: document' }) + parser_put.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + + //async delete + const parser_delete = subparsers.add_parser("delete", { "help": "delete command" }); + parser_delete.addArgument('uri', { nargs: '+', help: 'Request uri' }) + + //fields + const parser_fields = subparsers.add_parser("fields", { "help": "fields command" }); + parser_fields.addArgument('--type', { help: 'Display fields types for TYPE.' }) + + //searches + const parser_searches = subparsers.add_parser("searches", { "help": "searches command" }); + parser_searches.addArgument('--create', { nargs: 1, help: 'Path of JSON file containing the definitions of saved searches.' }) + + //tags + const parser_tags = subparsers.add_parser("tags", { "help": "tags command" }); + parser_tags.addArgument('--filter', { help: 'Tags of all types matching a specific name.' }) + parser_tags.addArgument('--count', { action: 'storeTrue', help: 'TODO: document' }) + + // TODO: reinstate key + /* + async function $key(argparse) { + //** + this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) + } + */ + + /* + + This was the previous method of getting sub-parsers. + + It extracts the sub-parser functions from each function. This is a + smart way of defining the interface. + + */ + /* + const subparsers = parser.addSubparsers({ title: 'commands', dest: 'command', required: true }) + // add all methods that do not start with _ as a command + for (const cmd of Object.getOwnPropertyNames(Object.getPrototypeOf(parser)).sort()) { + if (typeof parser[cmd] !== 'function' || cmd[0] !== '$') continue + const sp = subparsers.addParser(cmd.slice(1).replace(/_/g, '-'), { description: parser[cmd].__doc__, help: parser[cmd].__doc__ }) + // when called with an argparser, the command is expected to add relevant parameters and return + // the command must have a docstring + parser[cmd](sp) + } */ + + // Other URLs + // https://www.zotero.org/support/dev/web_api/v3/basics + // /keys/ + // /users//groups + /* + async function $key(argparse) { + /** Show details about this API key. (API: /keys ) */ +/* + this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) + }*/ + + + // Functions for get, post, put, patch, delete. (Delete query to API with uri.) + + /* + async function $get(argparser = null) { + /** Make a direct query to the API using 'GET uri'. */ +/* + if (argparser) { + argparser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) + argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + return + } + + for (const uri of this.args.uri) { + this.show(await this.get(uri, { userOrGroupPrefix: !this.args.root })) + } + } +*/ + + +/* + + async function $post(argparser = null) { + /** Make a direct query to the API using 'POST uri [--data data]'. */ +/* + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.post(this.args.uri, this.args.data)) + } +*/ + + +/* + async function $put(argparser = null) { + /** Make a direct query to the API using 'PUT uri [--data data]'. */ + /* + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.put(this.args.uri, this.args.data)) + } + + */ + + + /* + + async function $delete(argparser = null) { + /** Make a direct delete query to the API using 'DELETE uri'. */ +/* + if (argparser) { + argparser.addArgument('uri', { nargs: '+', help: 'Request uri' }) + return + } + + for (const uri of this.args.uri) { + const response = await this.get(uri) + await this.delete(uri, response.version) + } + } + + */ + + + //parser.set_defaults({ "func": new Zotero().run() }); + //this.parser.parse_args(); + + return parser.parseArgs(); + +} + + +// --- main --- +var args = getArguments() + +/* +if (args.verbose) { + console.log("zotero-cli starting...") +} +// zenodo-cli create --title "..." --authors "..." --dryrun +if (args.dryrun) { + console.log(`API command:\n Zotero.${args.func.name}(${JSON.stringify(args, null, 2)})`); +} else { + // ZenodoAPI.${args.func.name}(args) + args.func(args); +} +*/ + + + +//console.log(`Here...${zoterolib.Zotero}`); + +var test = new Zotero(args); +test.func(args).catch(err => { + console.error('error:', err) + process.exit(1) +}); + +/* +(zoterolib.zotero.run().catch(err => { + console.error('error:', err) + process.exit(1) +}) +) +*/ + +module.exports = { + node: 'current' +}; diff --git a/bin/BaCkUp/zotero-cli.210106.0006.d.ts b/bin/BaCkUp/zotero-cli.210106.0006.d.ts new file mode 100644 index 0000000..b798801 --- /dev/null +++ b/bin/BaCkUp/zotero-cli.210106.0006.d.ts @@ -0,0 +1,2 @@ +#!/usr/bin/env node +export {}; diff --git a/bin/BaCkUp/zotero-cli.210106.0006.ts b/bin/BaCkUp/zotero-cli.210106.0006.ts new file mode 100755 index 0000000..89f3df8 --- /dev/null +++ b/bin/BaCkUp/zotero-cli.210106.0006.ts @@ -0,0 +1,280 @@ +#!/usr/bin/env node + +import { parse } from '@iarna/toml'; +import * as argparse from 'argparse'; + +var Zotero = require('../zotero-lib/bin/zotero-api-lib'); + + +function getArguments() { + + const parser = new argparse.ArgumentParser({ "description": "Zotero command line utility" }); + parser.addArgument( + '--api-key', { help: 'The API key to access the Zotero API.' }) + parser.addArgument( + '--config', { type: argparse.file, help: 'Configuration file (toml format). Note that ./zotero-cli.toml and ~/.config/zotero-cli/zotero-cli.toml is picked up automatically.' }) + parser.addArgument( + '--user-id', { type: argparse.integer, help: 'The id of the user library.' }) + parser.addArgument('--group-id', { + action: 'store', + type: argparse.integer, + help: 'The id of the group library.' }) + // See below. If changed, add: You can provide the group-id as zotero-select link (zotero://...). Only the group-id is used, the item/collection id is discarded. + parser.addArgument( + '--indent', { type: argparse.integer, help: 'Identation for json output.' }) + parser.addArgument( + '--out', { help: 'Output to file' }) + parser.addArgument( + '--verbose', { action: 'storeTrue', help: 'Log requests.' }) + +/* +The following code explcitly adds subparsers. +Previously these were defined by the functions themselves (see below). +*/ + + //async $collections + const subparsers = parser.add_subparsers({ "help": "sub-command help" }); + const parser_collections = subparsers.add_parser("collections", { "help": "Collections command" }); + parser_collections.addArgument('--top', { action: 'storeTrue', help: 'Show only collection at top level.' }) + parser_collections.addArgument('--key', { nargs: 1, required: true, help: 'Show all the child collections of collection with key. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_collections.addArgument('--create-child', { nargs: '*', help: 'Create child collections of key (or at the top level if no key is specified) with the names specified.' }) + + //async $collection + const parser_collection = subparsers.add_parser("collection", { "help": "Collection command" }); + parser_collection.addArgument('--key', { nargs: 1, help: 'The key of the collection (required). You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_collection.addArgument('--tags', { action: 'storeTrue', help: 'Display tags present in the collection.' }) + parser_collection.addArgument('itemkeys', { nargs: '*' , help: 'Item keys for items to be added or removed from this collection.'}) + parser_collection.addArgument('--add', { nargs: '*', help: 'Add items to this collection. Note that adding items to collections with \'item --addtocollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + parser_collection.addArgument('--remove', { nargs: '*', help: 'Convenience method: Remove items from this collection. Note that removing items from collections with \'item --removefromcollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + + + //async items + const parser_items = subparsers.add_parser("items", { "help": "Items command" }); + parser_items.addArgument('--count', { action: 'storeTrue', help: 'Return the number of items.' }) + // argparser.addArgument('--all', { action: 'storeTrue', help: 'obsolete' }) + parser_items.addArgument('--filter', { type: argparse.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. For example: \'{"format": "json,bib", "limit": 100, "start": 100}\'.' }) + parser_items.addArgument('--collection', { help: 'Retrive list of items for collection. You can provide the collection key as a zotero-select link (zotero://...) to also set the group-id.' }) + parser_items.addArgument('--top', { action: 'storeTrue', help: 'Retrieve top-level items in the library/collection (excluding child items / attachments, excluding trashed items).' }) + parser_items.addArgument('--validate', { type: argparse.path, help: 'json-schema file for all itemtypes, or directory with schema files, one per itemtype.' }) + + //async item + const parser_item = subparsers.add_parser("item", { "help": "Item command" }); + parser_item.addArgument( + '--key', { "action": "store", + "required": true, + "help": 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_item.addArgument( + '--children', { action: 'storeTrue', help: 'Retrieve list of children for the item.' }) + parser_item.addArgument( + '--filter', { + type: argparse.json, + help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. To retrieve multiple items you have use "itemkey"; for example: \'{"format": "json,bib", "itemkey": "A,B,C"}\'. See https://www.zotero.org/support/dev/web_api/v3/basics#search_syntax.' }) + parser_item.addArgument('--addfile', { nargs: '*', help: 'Upload attachments to the item. (/items/new)' }) + parser_item.addArgument('--savefiles', { nargs: '*', help: 'Download all attachments from the item (/items/KEY/file).' }) + parser_item.addArgument('--addtocollection', { nargs: '*', help: 'Add item to collections. (Convenience method: patch item->data->collections.)' }) + parser_item.addArgument('--removefromcollection', { nargs: '*', help: 'Remove item from collections. (Convenience method: patch item->data->collections.)' }) + parser_item.addArgument('--addtags', { nargs: '*', help: 'Add tags to item. (Convenience method: patch item->data->tags.)' }) + parser_item.addArgument('--removetags', { nargs: '*', help: 'Remove tags from item. (Convenience method: patch item->data->tags.)' }) + + //async attachement + const parser_attachment = subparsers.add_parser("attachment", { "help": "Item command" }); + parser_attachment.addArgument('--key', { "action": "store", required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_attachment.addArgument('--save', { "action": "store", required: true, help: 'Filename to save attachment to.' }) + + //async create item + const parser_create = subparsers.add_parser("create", { "help": "Create command" }); + parser_create.addArgument('--template', { help: "Retrieve a template for the item you wish to create. You can retrieve the template types using the main argument 'types'." }) + parser_create.addArgument('items', { nargs: '*', help: 'Json files for the items to be created.' }) + + //update item + const parser_update = subparsers.add_parser("update", { "help": "update command" }); + parser_update.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser_update.addArgument('--replace', { action: 'storeTrue', help: 'Replace the item by sumbitting the complete json.' }) + parser_update.addArgument('items', { nargs: 1, help: 'Path of item files in json format.' }) + + //async get + const parser_get = subparsers.add_parser("get", { "help": "get command" }); + parser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) + parser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + + //async post + const parser_post = subparsers.add_parser("post", { "help": "post command" }); + parser_post.addArgument('uri', { action: "store", nargs: 1, help: 'TODO: document' }) + parser_post.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + + //async put + const parser_put = subparsers.add_parser("put", { "help": "put command" }); + parser_put.addArgument('uri', { action: "store", nargs: 1, help: 'TODO: document' }) + parser_put.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + + //async delete + const parser_delete = subparsers.add_parser("delete", { "help": "delete command" }); + parser_delete.addArgument('uri', { nargs: '+', help: 'Request uri' }) + + //fields + const parser_fields = subparsers.add_parser("fields", { "help": "fields command" }); + parser_fields.addArgument('--type', { help: 'Display fields types for TYPE.' }) + + //searches + const parser_searches = subparsers.add_parser("searches", { "help": "searches command" }); + parser_searches.addArgument('--create', { nargs: 1, help: 'Path of JSON file containing the definitions of saved searches.' }) + + //tags + const parser_tags = subparsers.add_parser("tags", { "help": "tags command" }); + parser_tags.addArgument('--filter', { help: 'Tags of all types matching a specific name.' }) + parser_tags.addArgument('--count', { action: 'storeTrue', help: 'TODO: document' }) + + // TODO: reinstate key + /* + async function $key(argparse) { + //** + this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) + } + */ + + /* + + This was the previous method of getting sub-parsers. + + It extracts the sub-parser functions from each function. This is a + smart way of defining the interface. + + */ + /* + const subparsers = parser.addSubparsers({ title: 'commands', dest: 'command', required: true }) + // add all methods that do not start with _ as a command + for (const cmd of Object.getOwnPropertyNames(Object.getPrototypeOf(parser)).sort()) { + if (typeof parser[cmd] !== 'function' || cmd[0] !== '$') continue + const sp = subparsers.addParser(cmd.slice(1).replace(/_/g, '-'), { description: parser[cmd].__doc__, help: parser[cmd].__doc__ }) + // when called with an argparser, the command is expected to add relevant parameters and return + // the command must have a docstring + parser[cmd](sp) + } */ + + // Other URLs + // https://www.zotero.org/support/dev/web_api/v3/basics + // /keys/ + // /users//groups + /* + async function $key(argparse) { + /** Show details about this API key. (API: /keys ) */ +/* + this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) + }*/ + + + // Functions for get, post, put, patch, delete. (Delete query to API with uri.) + + /* + async function $get(argparser = null) { + /** Make a direct query to the API using 'GET uri'. */ +/* + if (argparser) { + argparser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) + argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + return + } + + for (const uri of this.args.uri) { + this.show(await this.get(uri, { userOrGroupPrefix: !this.args.root })) + } + } +*/ + + +/* + + async function $post(argparser = null) { + /** Make a direct query to the API using 'POST uri [--data data]'. */ +/* + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.post(this.args.uri, this.args.data)) + } +*/ + + +/* + async function $put(argparser = null) { + /** Make a direct query to the API using 'PUT uri [--data data]'. */ + /* + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.put(this.args.uri, this.args.data)) + } + + */ + + + /* + + async function $delete(argparser = null) { + /** Make a direct delete query to the API using 'DELETE uri'. */ +/* + if (argparser) { + argparser.addArgument('uri', { nargs: '+', help: 'Request uri' }) + return + } + + for (const uri of this.args.uri) { + const response = await this.get(uri) + await this.delete(uri, response.version) + } + } + + */ + + + //parser.set_defaults({ "func": new Zotero().run() }); + //this.parser.parse_args(); + + return parser.parseArgs(); + +} + + +// --- main --- +var args = getArguments() + +/* +if (args.verbose) { + console.log("zotero-cli starting...") +} +// zenodo-cli create --title "..." --authors "..." --dryrun +if (args.dryrun) { + console.log(`API command:\n Zotero.${args.func.name}(${JSON.stringify(args, null, 2)})`); +} else { + // ZenodoAPI.${args.func.name}(args) + args.func(args); +} +*/ + + + +//console.log(`Here...${zoterolib.Zotero}`); + +var test = new Zotero(args); +test.func(args).catch(err => { + console.error('error:', err) + process.exit(1) +}); + +/* +(zoterolib.zotero.run().catch(err => { + console.error('error:', err) + process.exit(1) +}) +) +*/ + +module.exports = { + node: 'current' +}; diff --git a/bin/BaCkUp/zotero-cli.210106.d.ts b/bin/BaCkUp/zotero-cli.210106.d.ts new file mode 100644 index 0000000..b798801 --- /dev/null +++ b/bin/BaCkUp/zotero-cli.210106.d.ts @@ -0,0 +1,2 @@ +#!/usr/bin/env node +export {}; diff --git a/bin/BaCkUp/zotero-cli.210106.ts b/bin/BaCkUp/zotero-cli.210106.ts new file mode 100755 index 0000000..24f3971 --- /dev/null +++ b/bin/BaCkUp/zotero-cli.210106.ts @@ -0,0 +1,250 @@ +#!/usr/bin/env node +import { parse } from '@iarna/toml'; +import * as argparse from 'argparse'; + +var Zotero = require('../zotero-lib/bin/zotero-api-lib'); + + +function getArguments() { + + const parser = new argparse.ArgumentParser({ "description": "Zotero command line utility" }); + parser.addArgument('--api-key', { help: 'The API key to access the Zotero API.' }) + parser.addArgument('--config', { type: argparse.file, help: 'Configuration file (toml format). Note that ./zotero-cli.toml and ~/.config/zotero-cli/zotero-cli.toml is picked up automatically.' }) + parser.addArgument('--user-id', { type: argparse.integer, help: 'The id of the user library.' }) + parser.addArgument('--group-id', { type: argparse.integer, help: 'The id of the group library.' }) + // See below. If changed, add: You can provide the group-id as zotero-select link (zotero://...). Only the group-id is used, the item/collection id is discarded. + parser.addArgument('--indent', { type: argparse.integer, help: 'Identation for json output.' }) + parser.addArgument('--out', { help: 'Output to file' }) + parser.addArgument('--verbose', { action: 'storeTrue', help: 'Log requests.' }) + + //async $collections + + parser.addArgument('--top', { action: 'storeTrue', help: 'Show only collection at top level.' }) + //parser.addArgument('--key', { action:'',help: 'Show all the child collections of collection with key. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser.addArgument('--create-child', { nargs: '*', help: 'Create child collections of key (or at the top level if no key is specified) with the names specified.' }) + + //async $collection + + + //parser.addArgument('--key', { required: true, help: 'The key of the collection (required). You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser.addArgument('--tags', { action: 'storeTrue', help: 'Display tags present in the collection.' }) + // argparser.addArgument('itemkeys', { nargs: '*' , help: 'Item keys for items to be added or removed from this collection.'}) + parser.addArgument('--add', { nargs: '*', help: 'Add items to this collection. Note that adding items to collections with \'item --addtocollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + parser.addArgument('--remove', { nargs: '*', help: 'Convenience method: Remove items from this collection. Note that removing items from collections with \'item --removefromcollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }) + + + //async items + + parser.addArgument('--count', { action: 'storeTrue', help: 'Return the number of items.' }) + // argparser.addArgument('--all', { action: 'storeTrue', help: 'obsolete' }) + //parser.addArgument('--filter', { type: argparse.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. For example: \'{"format": "json,bib", "limit": 100, "start": 100}\'.' }) + parser.addArgument('--collection', { help: 'Retrive list of items for collection. You can provide the collection key as a zotero-select link (zotero://...) to also set the group-id.' }) + //parser.addArgument('--top', { action: 'storeTrue', help: 'Retrieve top-level items in the library/collection (excluding child items / attachments, excluding trashed items).' }) + parser.addArgument('--validate', { type: argparse.path, help: 'json-schema file for all itemtypes, or directory with schema files, one per itemtype.' }) + + //async item + + //parser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser.addArgument('--children', { action: 'storeTrue', help: 'Retrieve list of children for the item.' }) + //parser.addArgument('--filter', { type: argparse.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. To retrieve multiple items you have use "itemkey"; for example: \'{"format": "json,bib", "itemkey": "A,B,C"}\'. See https://www.zotero.org/support/dev/web_api/v3/basics#search_syntax.' }) + parser.addArgument('--addfile', { nargs: '*', help: 'Upload attachments to the item. (/items/new)' }) + parser.addArgument('--savefiles', { nargs: '*', help: 'Download all attachments from the item (/items/KEY/file).' }) + parser.addArgument('--addtocollection', { nargs: '*', help: 'Add item to collections. (Convenience method: patch item->data->collections.)' }) + parser.addArgument('--removefromcollection', { nargs: '*', help: 'Remove item from collections. (Convenience method: patch item->data->collections.)' }) + parser.addArgument('--addtags', { nargs: '*', help: 'Add tags to item. (Convenience method: patch item->data->tags.)' }) + parser.addArgument('--removetags', { nargs: '*', help: 'Remove tags from item. (Convenience method: patch item->data->tags.)' }) + + //async attachement + + parser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser.addArgument('--save', { required: true, help: 'Filename to save attachment to.' }) + + //async create item + + parser.addArgument('--template', { help: "Retrieve a template for the item you wish to create. You can retrieve the template types using the main argument 'types'." }) + parser.addArgument('items', { nargs: '*', help: 'Json files for the items to be created.' }) + + //update item + + + //parser.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }) + parser.addArgument('--replace', { action: 'storeTrue', help: 'Replace the item by sumbitting the complete json.' }) + parser.addArgument('items', { nargs: 1, help: 'Path of item files in json format.' }) + + + //async get + + parser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) + parser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + + //async post + + parser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + parser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + + + //async put + + parser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + //parser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + + //async delete + + parser.addArgument('uri', { nargs: '+', help: 'Request uri' }) + + + //fields + parser.addArgument('--type', { help: 'Display fields types for TYPE.' }) + + + //searches + + parser.addArgument('--create', { nargs: 1, help: 'Path of JSON file containing the definitions of saved searches.' }) + + //tags + parser.addArgument('--filter', { help: 'Tags of all types matching a specific name.' }) + //parser.addArgument('--count', { action: 'storeTrue', help: 'TODO: document' }) + + + + + const subparsers = parser.addSubparsers({ title: 'commands', dest: 'command', required: true }) + // add all methods that do not start with _ as a command + for (const cmd of Object.getOwnPropertyNames(Object.getPrototypeOf(parser)).sort()) { + if (typeof parser[cmd] !== 'function' || cmd[0] !== '$') continue + + const sp = subparsers.addParser(cmd.slice(1).replace(/_/g, '-'), { description: parser[cmd].__doc__, help: parser[cmd].__doc__ }) + // when called with an argparser, the command is expected to add relevant parameters and return + // the command must have a docstring + parser[cmd](sp) + +} + + + // Other URLs + // https://www.zotero.org/support/dev/web_api/v3/basics + // /keys/ + // /users//groups + /* + async function $key(argparse) { + /** Show details about this API key. (API: /keys ) */ +/* + this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) + }*/ + + + // Functions for get, post, put, patch, delete. (Delete query to API with uri.) + + /* + async function $get(argparser = null) { + /** Make a direct query to the API using 'GET uri'. */ +/* + if (argparser) { + argparser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) + argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + return + } + + for (const uri of this.args.uri) { + this.show(await this.get(uri, { userOrGroupPrefix: !this.args.root })) + } + } +*/ + + +/* + + async function $post(argparser = null) { + /** Make a direct query to the API using 'POST uri [--data data]'. */ +/* + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.post(this.args.uri, this.args.data)) + } +*/ + + +/* + async function $put(argparser = null) { + /** Make a direct query to the API using 'PUT uri [--data data]'. */ + /* + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.put(this.args.uri, this.args.data)) + } + + */ + + + /* + + async function $delete(argparser = null) { + /** Make a direct delete query to the API using 'DELETE uri'. */ +/* + if (argparser) { + argparser.addArgument('uri', { nargs: '+', help: 'Request uri' }) + return + } + + for (const uri of this.args.uri) { + const response = await this.get(uri) + await this.delete(uri, response.version) + } + } + + */ + + + //parser.set_defaults({ "func": new Zotero().run() }); + //this.parser.parse_args(); + + return parser.parseArgs(); + +} + + +// --- main --- +var args = getArguments() + +/* +if (args.verbose) { + console.log("zotero-cli starting...") +} +// zenodo-cli create --title "..." --authors "..." --dryrun +if (args.dryrun) { + console.log(`API command:\n Zotero.${args.func.name}(${JSON.stringify(args, null, 2)})`); +} else { + // ZenodoAPI.${args.func.name}(args) + args.func(args); +} +*/ + + + +//console.log(`Here...${zoterolib.Zotero}`); + +var test = new Zotero(args); +test.func(args).catch(err => { + console.error('error:', err) + process.exit(1) +}); + +/* +(zoterolib.zotero.run().catch(err => { + console.error('error:', err) + process.exit(1) +}) +) +*/ + +module.exports = { + node: 'current' +}; \ No newline at end of file diff --git a/bin/BaCkUp/zotero-cli.d.210106.d.ts b/bin/BaCkUp/zotero-cli.d.210106.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/bin/BaCkUp/zotero-cli.d.210106.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/bin/BaCkUp/zotero-cli.d.210106.ts b/bin/BaCkUp/zotero-cli.d.210106.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/bin/BaCkUp/zotero-cli.d.210106.ts @@ -0,0 +1 @@ +export {}; diff --git a/bin/zotero-cli.js~ b/bin/zotero-cli.js~ new file mode 100755 index 0000000..e2d1aa9 --- /dev/null +++ b/bin/zotero-cli.js~ @@ -0,0 +1,229 @@ +#!/usr/bin/env node +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const argparse = require("argparse"); +// var Zotero = require('../zotero-lib/bin/zotero-api-lib'); +var Zotero = require('zotero-lib'); +function getArguments() { + const parser = new argparse.ArgumentParser({ "description": "Zotero command line utility" }); + parser.addArgument('--api-key', { help: 'The API key to access the Zotero API.' }); + parser.addArgument('--config', { type: argparse.file, help: 'Configuration file (toml format). Note that ./zotero-cli.toml and ~/.config/zotero-cli/zotero-cli.toml is picked up automatically.' }); + parser.addArgument('--user-id', { type: argparse.integer, help: 'The id of the user library.' }); + parser.addArgument('--group-id', { + action: 'store', + type: argparse.integer, + help: 'The id of the group library.' + }); + // See below. If changed, add: You can provide the group-id as zotero-select link (zotero://...). Only the group-id is used, the item/collection id is discarded. + parser.addArgument('--indent', { type: argparse.integer, help: 'Identation for json output.' }); + parser.addArgument('--out', { help: 'Output to file' }); + parser.addArgument('--verbose', { action: 'storeTrue', help: 'Log requests.' }); + /* + The following code explcitly adds subparsers. + Previously these were defined by the functions themselves (see below). + */ + //async $collections + const subparsers = parser.add_subparsers({ "help": "sub-command help" }); + const parser_collections = subparsers.add_parser("collections", { "help": "Collections command" }); + parser_collections.addArgument('--top', { action: 'storeTrue', help: 'Show only collection at top level.' }); + parser_collections.addArgument('--key', { nargs: 1, required: true, help: 'Show all the child collections of collection with key. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }); + parser_collections.addArgument('--create-child', { nargs: '*', help: 'Create child collections of key (or at the top level if no key is specified) with the names specified.' }); + //async $collection + const parser_collection = subparsers.add_parser("collection", { "help": "Collection command" }); + parser_collection.addArgument('--key', { nargs: 1, help: 'The key of the collection (required). You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }); + parser_collection.addArgument('--tags', { action: 'storeTrue', help: 'Display tags present in the collection.' }); + parser_collection.addArgument('itemkeys', { nargs: '*', help: 'Item keys for items to be added or removed from this collection.' }); + parser_collection.addArgument('--add', { nargs: '*', help: 'Add items to this collection. Note that adding items to collections with \'item --addtocollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }); + parser_collection.addArgument('--remove', { nargs: '*', help: 'Convenience method: Remove items from this collection. Note that removing items from collections with \'item --removefromcollection\' may require fewer API queries. (Convenience method: patch item->data->collections.)' }); + //async items + const parser_items = subparsers.add_parser("items", { "help": "Items command" }); + parser_items.addArgument('--count', { action: 'storeTrue', help: 'Return the number of items.' }); + // argparser.addArgument('--all', { action: 'storeTrue', help: 'obsolete' }) + parser_items.addArgument('--filter', { type: argparse.json, help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. For example: \'{"format": "json,bib", "limit": 100, "start": 100}\'.' }); + parser_items.addArgument('--collection', { help: 'Retrive list of items for collection. You can provide the collection key as a zotero-select link (zotero://...) to also set the group-id.' }); + parser_items.addArgument('--top', { action: 'storeTrue', help: 'Retrieve top-level items in the library/collection (excluding child items / attachments, excluding trashed items).' }); + parser_items.addArgument('--validate', { type: argparse.path, help: 'json-schema file for all itemtypes, or directory with schema files, one per itemtype.' }); + //async item + const parser_item = subparsers.add_parser("item", { "help": "Item command" }); + parser_item.addArgument('--key', { "action": "store", + "required": true, + "help": 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }); + parser_item.addArgument('--children', { action: 'storeTrue', help: 'Retrieve list of children for the item.' }); + parser_item.addArgument('--filter', { + type: argparse.json, + help: 'Provide a filter as described in the Zotero API documentation under read requests / parameters. To retrieve multiple items you have use "itemkey"; for example: \'{"format": "json,bib", "itemkey": "A,B,C"}\'. See https://www.zotero.org/support/dev/web_api/v3/basics#search_syntax.' + }); + parser_item.addArgument('--addfile', { nargs: '*', help: 'Upload attachments to the item. (/items/new)' }); + parser_item.addArgument('--savefiles', { nargs: '*', help: 'Download all attachments from the item (/items/KEY/file).' }); + parser_item.addArgument('--addtocollection', { nargs: '*', help: 'Add item to collections. (Convenience method: patch item->data->collections.)' }); + parser_item.addArgument('--removefromcollection', { nargs: '*', help: 'Remove item from collections. (Convenience method: patch item->data->collections.)' }); + parser_item.addArgument('--addtags', { nargs: '*', help: 'Add tags to item. (Convenience method: patch item->data->tags.)' }); + parser_item.addArgument('--removetags', { nargs: '*', help: 'Remove tags from item. (Convenience method: patch item->data->tags.)' }); + //async attachement + const parser_attachment = subparsers.add_parser("attachment", { "help": "Item command" }); + parser_attachment.addArgument('--key', { "action": "store", required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }); + parser_attachment.addArgument('--save', { "action": "store", required: true, help: 'Filename to save attachment to.' }); + //async create item + const parser_create = subparsers.add_parser("create", { "help": "Create command" }); + parser_create.addArgument('--template', { help: "Retrieve a template for the item you wish to create. You can retrieve the template types using the main argument 'types'." }); + parser_create.addArgument('items', { nargs: '*', help: 'Json files for the items to be created.' }); + //update item + const parser_update = subparsers.add_parser("update", { "help": "update command" }); + parser_update.addArgument('--key', { required: true, help: 'The key of the item. You can provide the key as zotero-select link (zotero://...) to also set the group-id.' }); + parser_update.addArgument('--replace', { action: 'storeTrue', help: 'Replace the item by sumbitting the complete json.' }); + parser_update.addArgument('items', { nargs: 1, help: 'Path of item files in json format.' }); + //async get + const parser_get = subparsers.add_parser("get", { "help": "get command" }); + parser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }); + parser.addArgument('uri', { nargs: '+', help: 'TODO: document' }); + //async post + const parser_post = subparsers.add_parser("post", { "help": "post command" }); + parser_post.addArgument('uri', { action: "store", nargs: 1, help: 'TODO: document' }); + parser_post.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }); + //async put + const parser_put = subparsers.add_parser("put", { "help": "put command" }); + parser_put.addArgument('uri', { action: "store", nargs: 1, help: 'TODO: document' }); + parser_put.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }); + //async delete + const parser_delete = subparsers.add_parser("delete", { "help": "delete command" }); + parser_delete.addArgument('uri', { nargs: '+', help: 'Request uri' }); + //fields + const parser_fields = subparsers.add_parser("fields", { "help": "fields command" }); + parser_fields.addArgument('--type', { help: 'Display fields types for TYPE.' }); + //searches + const parser_searches = subparsers.add_parser("searches", { "help": "searches command" }); + parser_searches.addArgument('--create', { nargs: 1, help: 'Path of JSON file containing the definitions of saved searches.' }); + //tags + const parser_tags = subparsers.add_parser("tags", { "help": "tags command" }); + parser_tags.addArgument('--filter', { help: 'Tags of all types matching a specific name.' }); + parser_tags.addArgument('--count', { action: 'storeTrue', help: 'TODO: document' }); + // TODO: reinstate key + /* + async function $key(argparse) { + //** + this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) + } + */ + /* + + This was the previous method of getting sub-parsers. + + It extracts the sub-parser functions from each function. This is a + smart way of defining the interface. + + */ + /* + const subparsers = parser.addSubparsers({ title: 'commands', dest: 'command', required: true }) + // add all methods that do not start with _ as a command + for (const cmd of Object.getOwnPropertyNames(Object.getPrototypeOf(parser)).sort()) { + if (typeof parser[cmd] !== 'function' || cmd[0] !== '$') continue + const sp = subparsers.addParser(cmd.slice(1).replace(/_/g, '-'), { description: parser[cmd].__doc__, help: parser[cmd].__doc__ }) + // when called with an argparser, the command is expected to add relevant parameters and return + // the command must have a docstring + parser[cmd](sp) + } */ + // Other URLs + // https://www.zotero.org/support/dev/web_api/v3/basics + // /keys/ + // /users//groups + /* + async function $key(argparse) { + /** Show details about this API key. (API: /keys ) */ + /* + this.show(await this.get(`/keys/${this.args.api_key}`, { userOrGroupPrefix: false })) + }*/ + // Functions for get, post, put, patch, delete. (Delete query to API with uri.) + /* + async function $get(argparser = null) { + /** Make a direct query to the API using 'GET uri'. */ + /* + if (argparser) { + argparser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) + argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + return + } + + for (const uri of this.args.uri) { + this.show(await this.get(uri, { userOrGroupPrefix: !this.args.root })) + } + } + */ + /* + + async function $post(argparser = null) { + /** Make a direct query to the API using 'POST uri [--data data]'. */ + /* + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.post(this.args.uri, this.args.data)) + } + */ + /* + async function $put(argparser = null) { + /** Make a direct query to the API using 'PUT uri [--data data]'. */ + /* + if (argparser) { + argparser.addArgument('uri', { nargs: '1', help: 'TODO: document' }) + argparser.addArgument('--data', { required: true, help: 'Escaped JSON string for post data' }) + return + } + + this.print(await this.put(this.args.uri, this.args.data)) + } + + */ + /* + + async function $delete(argparser = null) { + /** Make a direct delete query to the API using 'DELETE uri'. */ + /* + if (argparser) { + argparser.addArgument('uri', { nargs: '+', help: 'Request uri' }) + return + } + + for (const uri of this.args.uri) { + const response = await this.get(uri) + await this.delete(uri, response.version) + } + } + + */ + //parser.set_defaults({ "func": new Zotero().run() }); + //this.parser.parse_args(); + return parser.parseArgs(); +} +// --- main --- +var args = getArguments(); +/* +if (args.verbose) { + console.log("zotero-cli starting...") +} +// zenodo-cli create --title "..." --authors "..." --dryrun +if (args.dryrun) { + console.log(`API command:\n Zotero.${args.func.name}(${JSON.stringify(args, null, 2)})`); +} else { + // ZenodoAPI.${args.func.name}(args) + args.func(args); +} +*/ +//console.log(`Here...${zoterolib.Zotero}`); +var test = new Zotero(args); +test.func(args).catch(err => { + console.error('error:', err); + process.exit(1); +}); +/* +(zoterolib.zotero.run().catch(err => { + console.error('error:', err) + process.exit(1) +}) +) +*/ +module.exports = { + node: 'current' +}; From f0572db2cafa8c2291fdbddc4fdda1cd52d81467 Mon Sep 17 00:00:00 2001 From: reyamme <47262195+reyamme@users.noreply.github.com> Date: Mon, 11 Jan 2021 13:51:54 +0000 Subject: [PATCH 10/10] sep functions --- bin/zotero-api-lib.ts | 26 +++++++++++++------------- bin/zotero-cli.ts | 27 +++++++++++++++++++-------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/bin/zotero-api-lib.ts b/bin/zotero-api-lib.ts index e88d579..757ce1b 100644 --- a/bin/zotero-api-lib.ts +++ b/bin/zotero-api-lib.ts @@ -788,10 +788,10 @@ export class Zotero { * Note that to retrieve a template, use 'create-item --template TYPE' rather than this command. */ - if (argparser) { - argparser.addArgument('--type', { help: 'Display fields types for TYPE.' }) - return - } + // if (argparser) { + // argparser.addArgument('--type', { help: 'Display fields types for TYPE.' }) + // return + // } if (this.args.type) { this.show(await this.get('/itemTypeFields', { params: { itemType: this.args.type }, userOrGroupPrefix: false })) @@ -807,10 +807,10 @@ export class Zotero { async $searches(argparser = null) { /** Return a list of the saved searches of the library. Create new saved searches. (API: /searches) */ - if (argparser) { - argparser.addArgument('--create', { nargs: 1, help: 'Path of JSON file containing the definitions of saved searches.' }) - return - } + // if (argparser) { + // argparser.addArgument('--create', { nargs: 1, help: 'Path of JSON file containing the definitions of saved searches.' }) + // return + // } if (this.args.create) { let searchDef = []; @@ -838,11 +838,11 @@ export class Zotero { async $tags(argparser = null) { /** Return a list of tags in the library. Options to filter and count tags. (API: /tags) */ - if (argparser) { - argparser.addArgument('--filter', { help: 'Tags of all types matching a specific name.' }) - argparser.addArgument('--count', { action: 'storeTrue', help: 'TODO: document' }) - return - } + // if (argparser) { + // argparser.addArgument('--filter', { help: 'Tags of all types matching a specific name.' }) + // argparser.addArgument('--count', { action: 'storeTrue', help: 'TODO: document' }) + // return + // } let rawTags = null; if (this.args.filter) { diff --git a/bin/zotero-cli.ts b/bin/zotero-cli.ts index 04d6a53..d1c1d25 100755 --- a/bin/zotero-cli.ts +++ b/bin/zotero-cli.ts @@ -109,6 +109,17 @@ function parArg(api) { sp.addArgument('--replace', { action: 'storeTrue', help: 'Replace the item by sumbitting the complete json.' }) sp.addArgument('items', { nargs: 1, help: 'Path of item files in json format.' }) } + if (cmd === "$tags") { + sp.addArgument('--filter', { help: 'Tags of all types matching a specific name.' }) + sp.addArgument('--count', { action: 'storeTrue', help: 'TODO: document' }) + } + if (cmd === "$searches") { + sp.addArgument('--create', { nargs: 1, help: 'Path of JSON file containing the definitions of saved searches.' }) + } + + if (cmd === "$fields") { + sp.addArgument('--type', { help: 'Display fields types for TYPE.' }) + } } else { @@ -123,7 +134,7 @@ function parArg(api) { async function $key(argparser = null) { -/** Show details about this API key. (API: /keys ) */ + /** Show details about this API key. (API: /keys ) */ if (argparser) return @@ -134,13 +145,15 @@ async function $key(argparser = null) { async function $get(argparser = null) { /** Make a direct query to the API using 'GET uri'. */ -console.log("rrrrrrr") + console.log("rrrrrrr") - if (argparser) { + if (argparser) { argparser.addArgument('--root', { action: 'storeTrue', help: 'TODO: document' }) -argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) + argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) return -} + } + + @@ -163,8 +176,6 @@ argparser.addArgument('uri', { nargs: '+', help: 'TODO: document' }) - - for (const uri of this.args.uri) { this.show(await this.get(uri, { userOrGroupPrefix: !this.args.root })) } @@ -213,7 +224,7 @@ ee.parser = parArg(ee) ee.args = parArg(ee).parseArgs() ee.run( - ).catch(err => { +).catch(err => { console.error('error:', err) process.exit(1) })