From 7e87476a82febe3afa49a81cd1a85cb675722e30 Mon Sep 17 00:00:00 2001 From: Adrien Maret Date: Thu, 25 Jun 2020 04:40:44 +0200 Subject: [PATCH] Add query hook for any API method (#53) ## What does this PR do? When no command is found, Kourou will try to execute the given command with the `sdk:query` command. The first argument has to be the name of the controller and the action separated by a semicolon (eg `document:create`) Kourou will try to infer common arguments like `index`, `collection`, `_id` or `body`. It will automatically infer and accept the following lists of arguments: - ` ` * _eg: `kourou collection:list iot`_ . - ` ` * _eg: `kourou collection:truncate iot sensors`_ . - ` ` * _eg: `kourou bulk:import iot sensors '{bulkData: []}'`_ . - ` ` * _eg: `kourou document:delete iot sensors sigfox-123`_ . - ` ` * _eg: `kourou document:create iot sensors sigfox-123 '{temperature: 42}'`_ Then any argument will be passed as-is to the `sdk:query` method. ### Other changes - add `--id` argument to `sdk:query` - remove `document:get` - remove `document:create` --- README.md | 74 ++++++++++++-- features/ApiMethodHook.feature | 47 +++++++++ features/Collection.feature | 6 +- features/Document.feature | 35 ------- features/support/hooks.js | 6 +- package.json | 5 +- src/commands/collection/create.ts | 11 +- src/commands/document/create.ts | 60 ----------- src/commands/document/get.ts | 28 ----- src/commands/sdk/query.ts | 41 ++++++-- src/hooks/command_not_found/api-method.ts | 118 ++++++++++++++++++++++ 11 files changed, 277 insertions(+), 154 deletions(-) create mode 100644 features/ApiMethodHook.feature delete mode 100644 src/commands/document/create.ts delete mode 100644 src/commands/document/get.ts create mode 100644 src/hooks/command_not_found/api-method.ts diff --git a/README.md b/README.md index 646e3294..2263374a 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,40 @@ $ kourou sdk:query auth:getCurrentUser --as gordon --username admin --password a [...] ``` +## Automatic command infering for API methods + +When no command is found, Kourou will try to execute the given command with the `sdk:query` command. + +The first argument has to be the name of the controller and the action separated by a semicolon (eg `document:create`) + +Kourou will try to infer common arguments like `index`, `collection`, `_id` or `body`. + +It will automatically infer and accept the following lists of arguments: + - ` ` + * _eg: `kourou collection:list iot`_ +. + + + - ` ` + * _eg: `kourou collection:truncate iot sensors`_ +. + + + - ` ` + * _eg: `kourou bulk:import iot sensors '{bulkData: []}'`_ +. + + + - ` ` + * _eg: `kourou document:delete iot sensors sigfox-123`_ +. + + + - ` ` + * _eg: `kourou document:create iot sensors sigfox-123 '{temperature: 42}'`_ + +Then any argument will be passed as-is to the `sdk:query` method. + # Commands @@ -861,6 +895,8 @@ OPTIONS --host=host [default: localhost] Kuzzle server host + --id=id ID argument (_id) + --password=password Kuzzle user password --port=port [default: 7512] Kuzzle server port @@ -876,16 +912,17 @@ DESCRIPTION Query arguments - arguments can be passed and repeated using the --arg or -a flag. - index and collection names can be passed with --index (-i) and --collection (-c) flags + Arguments can be passed and repeated using the --arg or -a flag. + Index and collection names can be passed with --index (-i) and --collection (-c) flags + ID can be passed with the --id flag. Examples: - - kourou sdk:query document:get -i iot -c sensors -a _id=sigfox-42 + - kourou sdk:query document:delete -i iot -c sensors -a refresh=wait_for Query body - body can be passed with the --body flag with either a JSON or JS string. - body will be read from STDIN if available + Body can be passed with the --body flag with either a JSON or JS string. + Body will be read from STDIN if available Examples: - kourou sdk:query document:create -i iot -c sensors --body '{creation: Date.now())}' @@ -894,12 +931,29 @@ DESCRIPTION Other - use the --editor flag to modify the query before sending it to Kuzzle - use the --display flag to display a specific property of the response + Use the --editor flag to modify the query before sending it to Kuzzle + Use the --display flag to display a specific property of the response Examples: - kourou sdk:query document:create -i iot -c sensors --editor - kourou sdk:query server:now --display 'result.now' + + Default fallback to API method + + It's possible to use this command by only specifying the corresponding controller + and action as first argument. + Kourou will try to infer the 4th first arguments to one the following: + - + - + If a flag is given (-i, -c, --body or --id), then the flag value has prior to + argument infering. + + Examples: + - kourou document:createOrReplace iot sensors sigfox-1 '{}' + - kourou collection:delete iot sensors + - kourou collection:list iot + - kourou bulk:import iot sensors '{bulkData: [...]}' + - kourou admin:loadMappings < mappings.json ``` _See code: [src/commands/sdk/query.ts](src/commands/sdk/query.ts)_ @@ -985,10 +1039,10 @@ DESCRIPTION The users will be exported WITHOUT their credentials since Kuzzzle can't access them. You can either: - - manually re-create credentials for your users - - use the "mustChangePasswordIfSetByAdmin" option Kuzzle password policies (see + - Manually re-create credentials for your users + - Use the "mustChangePasswordIfSetByAdmin" option Kuzzle password policies (see https://github.com/kuzzleio/kuzzle-plugin-auth-passport-local/#optional-properties) - - use the "--generate-credentials" flag to auto-generate credentials for your users + - Use the "--generate-credentials" flag to auto-generate credentials for your users Auto-generation of credentials diff --git a/features/ApiMethodHook.feature b/features/ApiMethodHook.feature new file mode 100644 index 00000000..92c08cb8 --- /dev/null +++ b/features/ApiMethodHook.feature @@ -0,0 +1,47 @@ +Feature: Hooks + + # command_not_found hook ===================================================== + + @mappings + Scenario: Unregistered API method + Given an existing collection "nyc-open-data":"yellow-taxi" + When I run the command "document:createOrReplace" with: + | flag | --arg | index=nyc-open-data | + | flag | --arg | collection=yellow-taxi | + | flag | -a | _id=chuon-chuon-kim | + | flag | --body | { "other-name": "my" } | + Then The document "chuon-chuon-kim" content match: + | other-name | "my" | + + @mappings + Scenario: Infer common arguments + Given an existing collection "nyc-open-data":"yellow-taxi" + # + When I run the command "collection:list" with: + | arg | nyc-open-data | | + Then I should match stdout with "yellow-taxi" + # + When I run the command "collection:truncate" with: + | arg | nyc-open-data | | + | arg | yellow-taxi | | + Then I should match stdout with "acknowledged" + # + When I run the command "document:createOrReplace" with: + | arg | nyc-open-data | | + | arg | yellow-taxi | | + | arg | foobar-1 | | + | arg | {helloWorld: 42} | | + Then I should match stdout with "helloWorld" + # + When I run the command "document:delete" with: + | arg | nyc-open-data | | + | arg | yellow-taxi | | + | arg | foobar-1 | | + Then I should match stdout with "foobar-1" + # + When I run the command "collection:updateMapping" with: + | arg | nyc-open-data | | + | arg | yellow-taxi | | + | arg | { dynamic: "false" } | | + Then I should match stdout with "false" + diff --git a/features/Collection.feature b/features/Collection.feature index 7c5a0131..686e0f18 100644 --- a/features/Collection.feature +++ b/features/Collection.feature @@ -39,9 +39,9 @@ Feature: Collection Commands Scenario: Creates a collection When I run the command "collection:create" with: - | arg | mtp-open-data | | - | arg | yellow-taxi | | - | flag | --body | { mappings: { dynamic: false } } | + | arg | mtp-open-data | | + | arg | yellow-taxi | | + | arg | { mappings: { dynamic: "false" } } | | And I successfully call the route "collection":"getMapping" with args: | index | "mtp-open-data" | | collection | "yellow-taxi" | diff --git a/features/Document.feature b/features/Document.feature index bee29ef0..d776ffdb 100644 --- a/features/Document.feature +++ b/features/Document.feature @@ -1,40 +1,5 @@ Feature: Document Management - # document:create ============================================================ - - @mappings - Scenario: Creates a document - Given an existing collection "nyc-open-data":"yellow-taxi" - When I run the command "document:create" with: - | arg | nyc-open-data | | - | arg | yellow-taxi | | - | flag | --id | chuon-chuon-kim | - | flag | --body | { "my": "name" } | - Then The document "chuon-chuon-kim" content match: - | my | "name" | - When I run the command "document:create" with: - | arg | nyc-open-data | | - | arg | yellow-taxi | | - | flag | --id | chuon-chuon-kim | - | flag | --body | { "my": "other name" } | - | flag | --replace | | - Then The document "chuon-chuon-kim" content match: - | my | "other name" | - - # document:get =============================================================== - - @mappings - Scenario: Gets a document - Given an existing collection "nyc-open-data":"yellow-taxi" - And I create the following document: - | _id | "chuon-chuon-kim" | - | body | {} | - When I run the command "document:get" with args: - | "nyc-open-data" | - | "yellow-taxi" | - | "chuon-chuon-kim" | - Then I should match stdout with "chuon-chuon-kim" - # document:search ============================================================ @mappings diff --git a/features/support/hooks.js b/features/support/hooks.js index 64d42457..f86e5c96 100644 --- a/features/support/hooks.js +++ b/features/support/hooks.js @@ -21,7 +21,8 @@ async function resetSecurityDefault(sdk) { controller: 'admin', action: 'loadSecurities', body: testSecurities, - refresh: 'wait_for' + refresh: 'wait_for', + onExistingUsers: 'overwrite' }); await sdk.auth.login( @@ -46,7 +47,8 @@ BeforeAll(({ timeout: 10 * 1000 }), async function () { controller: 'admin', action: 'loadSecurities', body: testSecurities, - refresh: 'wait_for' + refresh: 'wait_for', + onExistingUsers: 'overwrite' }); world.sdk.disconnect(); diff --git a/package.json b/package.json index 819ef250..be876170 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,9 @@ "sdk": { "description": "directly manipulate the sdk" } + }, + "hooks": { + "command_not_found": "./lib/hooks/command_not_found/api-method" } }, "repository": "kuzzleio/kourou", @@ -117,4 +120,4 @@ "test:functional:cucumber": "./node_modules/.bin/cucumber-js" }, "types": "lib/index.d.ts" -} +} \ No newline at end of file diff --git a/src/commands/collection/create.ts b/src/commands/collection/create.ts index cbca97bb..848a7b9b 100644 --- a/src/commands/collection/create.ts +++ b/src/commands/collection/create.ts @@ -8,22 +8,19 @@ export default class CollectionCreate extends Kommand { static flags = { help: flags.help(), - body: flags.string({ - description: 'Collection mappings and settings in JS or JSON format. Will be read from STDIN if available', - default: '{}' - }), ...kuzzleFlags } static args = [ { name: 'index', description: 'Index name', required: true }, - { name: 'collection', description: 'Collection name', required: true } + { name: 'collection', description: 'Collection name', required: true }, + { name: 'body', description: 'Collection mappings and settings in JS or JSON format. Will be read from STDIN if available' }, ] static readStdin = true async runSafe() { - const body = this.stdin ? this.parseJs(this.stdin) : this.parseJs(this.flags.body) + const body = this.stdin ? this.stdin : this.args.body || '{}' if (!await this.sdk?.index.exists(this.args.index)) { await this.sdk?.index.create(this.args.index) @@ -31,7 +28,7 @@ export default class CollectionCreate extends Kommand { this.logInfo(`Index "${this.args.index}" created`) } - await this.sdk?.collection.create(this.args.index, this.args.collection, body) + await this.sdk?.collection.create(this.args.index, this.args.collection, this.parseJs(body)) this.logOk(`Collection "${this.args.index}":"${this.args.collection}" created`) } diff --git a/src/commands/document/create.ts b/src/commands/document/create.ts deleted file mode 100644 index 2bb0a90d..00000000 --- a/src/commands/document/create.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { flags } from '@oclif/command' - -import { Kommand } from '../../common' -import { kuzzleFlags } from '../../support/kuzzle' - -export default class DocumentCreate extends Kommand { - static description = 'Creates a document' - - static examples = [ - 'kourou document:create iot sensors --body \'{network: "sigfox"}\'', - 'kourou document:create iot sensors < document.json', - ] - - static flags = { - body: flags.string({ - description: 'Document body in JS or JSON format. Will be read from STDIN if available', - default: '{}' - }), - id: flags.string({ - description: 'Optional document ID' - }), - replace: flags.boolean({ - description: 'Replaces the document if it already exists' - }), - help: flags.help(), - ...kuzzleFlags - } - - static args = [ - { name: 'index', description: 'Index name', required: true }, - { name: 'collection', description: 'Collection name', required: true } - ] - - static readStdin = true - - async runSafe() { - const body = this.stdin ? this.stdin : this.flags.body - - if (this.flags.replace) { - const document = await this.sdk?.document.replace( - this.args.index, - this.args.collection, - this.flags.id, - this.parseJs(body), - { refresh: 'wait_for' }) - - this.logOk(`Document "${document._id}" successfully replaced`) - } - else { - const document = await this.sdk?.document.create( - this.args.index, - this.args.collection, - this.parseJs(body), - this.flags.id, - { refresh: 'wait_for' }) - - this.logOk(`Document "${document._id}" successfully created`) - } - } -} diff --git a/src/commands/document/get.ts b/src/commands/document/get.ts deleted file mode 100644 index 4ff3ae85..00000000 --- a/src/commands/document/get.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { flags } from '@oclif/command' - -import { Kommand } from '../../common' -import { kuzzleFlags } from '../../support/kuzzle' - -export default class DocumentGet extends Kommand { - static description = 'Gets a document' - - static flags = { - help: flags.help(), - ...kuzzleFlags - } - - static args = [ - { name: 'index', description: 'Index name', required: true }, - { name: 'collection', description: 'Collection name', required: true }, - { name: 'id', description: 'Document ID', required: true } - ] - - async runSafe() { - const document = await this.sdk?.document.get( - this.args.index, - this.args.collection, - this.args.id) - - this.log(JSON.stringify(document, null, 2)) - } -} diff --git a/src/commands/sdk/query.ts b/src/commands/sdk/query.ts index d379c8d6..13b72279 100644 --- a/src/commands/sdk/query.ts +++ b/src/commands/sdk/query.ts @@ -1,5 +1,5 @@ import { flags } from '@oclif/command' -import _ from 'lodash' +import _ from 'lodash' import { Kommand } from '../../common' import { kuzzleFlags } from '../../support/kuzzle' @@ -10,16 +10,17 @@ Executes an API query. Query arguments - arguments can be passed and repeated using the --arg or -a flag. - index and collection names can be passed with --index (-i) and --collection (-c) flags + Arguments can be passed and repeated using the --arg or -a flag. + Index and collection names can be passed with --index (-i) and --collection (-c) flags + ID can be passed with the --id flag. Examples: - - kourou sdk:query document:get -i iot -c sensors -a _id=sigfox-42 + - kourou sdk:query document:delete -i iot -c sensors -a refresh=wait_for Query body - body can be passed with the --body flag with either a JSON or JS string. - body will be read from STDIN if available + Body can be passed with the --body flag with either a JSON or JS string. + Body will be read from STDIN if available Examples: - kourou sdk:query document:create -i iot -c sensors --body '{creation: Date.now())}' @@ -28,12 +29,32 @@ Query body Other - use the --editor flag to modify the query before sending it to Kuzzle - use the --display flag to display a specific property of the response + Use the --editor flag to modify the query before sending it to Kuzzle + Use the --display flag to display a specific property of the response Examples: - kourou sdk:query document:create -i iot -c sensors --editor - kourou sdk:query server:now --display 'result.now' + +Default fallback to API method + + It's possible to use this command by only specifying the corresponding controller + and action as first argument. + Kourou will try to infer the first arguments to one the following pattern: + - + - + - + - + - + If a flag is given (-i, -c, --body or --id), then the flag value has prior to + argument infering. + + Examples: + - kourou collection:list iot + - kourou collection:delete iot sensors + - kourou document:createOrReplace iot sensors sigfox-1 '{}' + - kourou bulk:import iot sensors '{bulkData: [...]}' + - kourou admin:loadMappings < mappings.json `; public static flags = { @@ -58,6 +79,9 @@ Other char: 'c', description: 'Collection argument' }), + id: flags.string({ + description: 'ID argument (_id)' + }), display: flags.string({ description: 'Path of the property to display from the response (empty string to display everything)', default: 'result' @@ -82,6 +106,7 @@ Other requestArgs.index = this.flags.index requestArgs.collection = this.flags.collection + requestArgs._id = this.flags.id for (const keyValue of this.flags.arg || []) { const [key, ...value] = keyValue.split('=') diff --git a/src/hooks/command_not_found/api-method.ts b/src/hooks/command_not_found/api-method.ts new file mode 100644 index 00000000..1e5b5116 --- /dev/null +++ b/src/hooks/command_not_found/api-method.ts @@ -0,0 +1,118 @@ +import chalk from 'chalk' +import { Hook } from '@oclif/config' +import SdkQuery from '../../commands/sdk/query' + +/** + * Hooks that run the corresponding API method with sdk:query. + * + * Example: + * - kourou document:create -i index -c collection --id foobar-1 --body '{}' + * + * Index, collection, ID and body can be infered if they are + * passed as argument in one of the following order: + * - + * - + * - + * - + * - + * + * This is mainly to match easily methods from the document, bulk, realtime + * and collection controllers. + * + * If one of the infered arguments is passed as a flag, then Kourou will + * use the flag value and will not try to infere arguments. + * + * Example: + * - kourou document:create index collection foobar-1 '{}' + * - kourou bulk:import index collection '{bulkData: []}' + */ + +async function callSdkQuery(args: string[]) { + try { + await SdkQuery.run(args) + + // eslint-disable-next-line + process.exit(0) + } + catch (error) { + // eslint-disable-next-line + process.exit(1) + } +} + +const hook: Hook<'command_not_found'> = async function (opts) { + const [controller, action] = opts.id.split(':') + + if (!controller || !action) { + return + } + + this.log(chalk.yellow(`[ℹ] Unknown command "${opts.id}", fallback to API method`)) + + const args = process.argv.slice(3) + const commandArgs = [opts.id] + + // first positional argument (index) + if (args[0] + && args[0].charAt(0) !== '-' + && !args.includes('-i') + && !args.includes('--index') + ) { + commandArgs.push('-i') + commandArgs.push(args[0]) + + args.splice(0, 1) + } + else { + return callSdkQuery([...commandArgs, ...args]) + } + + // 2th positional argument (collection) + if (args[0] + && args[0].charAt(0) !== '-' + && !args.includes('-c') + && !args.includes('--collection') + ) { + commandArgs.push('-c') + commandArgs.push(args[0]) + + args.splice(0, 1) + } + else { + return callSdkQuery([...commandArgs, ...args]) + } + + // 3th positional argument (_id or body) + if (args[0] && args[0].charAt(0) !== '-') { + if (args[0].includes('{') && !args.includes('--body')) { + commandArgs.push('--body') + commandArgs.push(args[0]) + + args.splice(0, 1) + } + else if (!args.includes('--id')) { + commandArgs.push('--id') + commandArgs.push(args[0]) + + args.splice(0, 1) + } + } + else { + return callSdkQuery([...commandArgs, ...args]) + } + + // 4th positional argument (body) + if (args[0] + && args[0].charAt(0) !== '-' + && !commandArgs.includes('--body') + ) { + commandArgs.push('--body') + commandArgs.push(args[0]) + + args.splice(0, 1) + } + + return callSdkQuery([...commandArgs, ...args]) +} + +export default hook