diff --git a/package.json b/package.json index be6482f..5ef5e0d 100644 --- a/package.json +++ b/package.json @@ -1,60 +1,67 @@ { - "name": "pcli", - "type": "module", - "scripts": { - "test:backend": "npx ts-node-esm test/server.ts", - "build": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json", - "build:bin": "npm run build && npm run prepack:esbuild:cjs && npm run prepack:pkg", - "build:bin:all": "npm run build && npm run prepack:esbuild:cjs && npm run prepack:pkg:all", - "dev": "tsc-watch --noClear -p tsconfig.json", - "prepack:webpack": "npx webpack --config webpack.config.cjs", - "prepack:pkg:all": "npx pkg --options 'no-warnings' -t node18-linux-x64,node18-macos-x64,node18-win-x64 ./dist/bundle.cjs -o ./bin/pcli", - "prepack:pkg": "npx pkg --options 'no-warnings' -t node18 ./dist/bundle.cjs -o ./bin/pcli", - "prepack:esbuild:cjs": "npx esbuild dist/src/index.js --bundle --outfile=dist/bundle.cjs --format=cjs --platform=node", - "prepack:esbuild:esm": "npx esbuild dist/src/index.js --bundle --outfile=dist/bundle.mjs --format=esm --platform=node --banner:js=\"import {createRequire} from 'module'; const require = createRequire(import.meta.url); import { dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url));\"" - }, - "dependencies": { - "axios": "^0.27.2", - "browserify": "^17.0.0", - "chai": "^4.3.7", - "chalk": "^5.2.0", - "clean-stack": "^5.1.0", - "commander": "^9.4.0", - "content-type": "^1.0.4", - "dotenv": "^16.0.1", - "enquirer": "^2.3.6", - "fs-extra": "^11.1.0", - "lodash": "^4.17.21", - "newman": "^5.3.2", - "postman-collection": "^4.1.5", - "pretty-bytes": "^6.1.0", - "terser": "^5.16.5", - "uuid": "^9.0.0", - "winston": "^3.8.2" - }, - "devDependencies": { - "@babel/cli": "^7.18.10", - "@babel/core": "^7.18.10", - "@inquirer/editor": "^0.0.21-alpha.0", - "@types/chai": "^4.3.4", - "@types/content-type": "^1.1.5", - "@types/express": "^4.17.17", - "@types/fs-extra": "^9.0.13", - "@types/lodash": "^4.14.182", - "@types/newman": "^5.3.1", - "@types/postman-collection": "^3.5.7", - "@types/uuid": "^9.0.0", - "@typescript-eslint/eslint-plugin": "^5.51.0", - "@typescript-eslint/parser": "^5.51.0", - "eslint": "^8.33.0", - "express": "^4.18.2", - "pkg": "^5.8.0", - "ts-node": "^10.9.1", - "tsc-alias": "^1.8.2", - "tsc-watch": "^5.0.3", - "typescript": "^4.7.4" - }, - "bundleDependencies": [ - "terser" - ] + "name": "pcli", + "type": "module", + "main": "./src/index.js", + "scripts": { + "test:backend": "npx ts-node-esm test/server.ts", + "build": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json", + "build:bin": "npm run build && npm run prepack:esbuild:cjs && npm run prepack:pkg", + "build:bin:all": "npm run build && npm run prepack:esbuild:cjs && npm run prepack:pkg:all", + "dev": "tsc-watch --noClear -p tsconfig.json", + "prepack:webpack": "npx webpack --config webpack.config.cjs", + "prepack:pkg:all": "npx pkg --options 'no-warnings' -t node18-linux-x64,node18-macos-x64,node18-win-x64 ./dist/bundle.cjs -o ./bin/pcli", + "prepack:pkg": "npx pkg --options 'no-warnings' -t node18 ./dist/bundle.cjs -o ./bin/pcli", + "prepack:esbuild:cjs": "npx esbuild dist/src/index.js --bundle --outfile=dist/bundle.cjs --format=cjs --platform=node", + "prepack:esbuild:esm": "npx esbuild dist/src/index.js --bundle --outfile=dist/bundle.mjs --format=esm --platform=node --banner:js=\"import {createRequire} from 'module'; const require = createRequire(import.meta.url); import { dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url));\"" + }, + "dependencies": { + "axios": "^0.27.2", + "browserify": "^17.0.0", + "chai": "^4.3.7", + "chalk": "^5.2.0", + "clean-stack": "^5.1.0", + "commander": "^9.4.0", + "content-type": "^1.0.4", + "dotenv": "^16.0.1", + "enquirer": "^2.3.6", + "fs-extra": "^11.1.0", + "js-object-pretty-print": "^0.3.0", + "lodash": "^4.17.21", + "newman": "^5.3.2", + "postman-collection": "^4.1.5", + "pretty-bytes": "^6.1.0", + "terser": "^5.16.5", + "tmp": "^0.2.1", + "uuid": "^9.0.0", + "winston": "^3.8.2" + }, + "devDependencies": { + "@babel/cli": "^7.18.10", + "@babel/core": "^7.18.10", + "@inquirer/editor": "^0.0.21-alpha.0", + "@types/chai": "^4.3.4", + "@types/content-type": "^1.1.5", + "@types/express": "^4.17.17", + "@types/fs-extra": "^9.0.13", + "@types/lodash": "^4.14.182", + "@types/newman": "^5.3.1", + "@types/postman-collection": "^3.5.7", + "@types/tmp": "^0.2.3", + "@types/uuid": "^9.0.0", + "@typescript-eslint/eslint-plugin": "^5.54.1", + "@typescript-eslint/parser": "^5.54.1", + "esbuild": "^0.17.11", + "eslint": "^8.33.0", + "express": "^4.18.2", + "open": "^8.4.2", + "open-editor": "^4.0.0", + "pkg": "^5.8.0", + "ts-node": "^10.9.1", + "tsc-alias": "^1.8.2", + "tsc-watch": "^5.0.3", + "typescript": "^4.7.4" + }, + "bundleDependencies": [ + "terser" + ] } diff --git a/src/handlers/update.ts b/src/handlers/update.ts index 7cbe2ab..87851db 100644 --- a/src/handlers/update.ts +++ b/src/handlers/update.ts @@ -1,34 +1,99 @@ import services from '@src/services/index.js' import psdk from 'postman-collection' -import { PostmanCli } from '@src/types' -import { Command } from 'commander' -import editor from '@inquirer/editor' +import {PostmanCli} from '@src/types' +import {Command} from 'commander' import util from 'node:util' +import _ from 'lodash' +import Enquirer from 'enquirer' +import openeditor from 'open-editor' +import editor from '@inquirer/editor' +import open from 'open' +import tmp from 'tmp' +import fs from 'node:fs' + +function openJson(input: string) { + const {name} = tmp.fileSync({postfix: '.js'}) + fs.writeFileSync(name, input) + + //openeditor([{file: name}]) + open(name) + return name +} export default async function ( - args: PostmanCli.Cmd.VariadicResources, - ..._cmd: [PostmanCli.Cmd.Opts.Update, Command] + args: PostmanCli.Cmd.VariadicResources, + ..._cmd: [PostmanCli.Cmd.Opts.Update, Command] ) { - const [optional, cmd] = _cmd - args = args.map(e => e.toLowerCase()) - const co = await services.cmdopts.getOptCollection(cmd) - const resource = services.resource.getFromNested(co, args) - - if (services.response.isResponse(resource)) { - const p = services.example.toPrintable(resource) - const str = util.inspect(p, { - colors: false, - maxArrayLength: null, - maxStringLength: null, - depth: 50, - }) - const prompt: any = await editor({ default: str, message: '' }) - - new psdk.Response({ - code: prompt.response.code, - responseTime: prompt.response.time, - body: prompt.response.body, - header: [{}], - }) - } + const [optional, cmd] = _cmd + args = args.map(e => e.toLowerCase()) + const co = await services.cmdopts.getOptCollection(cmd) + const resource = services.resource.getFromNested(co, args) + + if (services.response.isResponse(resource)) { + const p = services.example.toPrintable(resource, {addParsedBody: true}) + + const str = util.inspect(p, { + colors: false, + maxArrayLength: null, + maxStringLength: null, + depth: 50, + }) + //const prompt = await editor({default: str, message: ''}) + const filename = openJson(str) + let done: boolean + try { + const enq = await Enquirer.prompt({name: 'done', required: true, initial: true, message: 'press any key to continue', type: 'confirm'}) as any + done = enq.done + } catch (err) {done = false} + + if (!done) return + + type ParsedPrompt = {request: PostmanCli.RequestPrintable, response: PostmanCli.ResponsePrintable} + let prompt: ParsedPrompt + try { + prompt = eval("(" + await fs.promises.readFile(filename, 'utf8') + ")") + } catch (e) {throw Error('could not parse')} + + const parsed = prompt + //const parsed: ParsedPrompt = JSON.parse(prompt) + + let request: psdk.RequestDefinition | undefined + if (parsed.request && !_.isEmpty(parsed.request)) { + const rawBody = JSON.stringify(parsed.request.body) + request = { + url: {path: parsed.request.url.path, query: services.common.jsonToHeaders(parsed.request.query)}, + body: {mode: 'raw', raw: rawBody}, + header: services.common.jsonToHeaders(parsed.request.headers), + method: parsed.request.url.method, + + } + } + const response = new psdk.Response({ + code: parsed.response.code, + responseTime: parsed.response.time, + body: JSON.stringify(parsed.response.body), + header: services.common.jsonToHeaders(parsed.response.headers), + + originalRequest: request + }) + + + services.example.print(response) + const fn = async () => { + try { + const {saveOrNot} = await Enquirer.prompt({ + name: 'saveOrNot', type: 'select', message: 'save changes?', + choices: [{name: 'yes'}, {name: 'no'}] + }) as any + + return saveOrNot + } catch (e) {return 'no'} + } + const saveOrNot = await fn() + + if (saveOrNot == 'yes') { + resource.update(response) + await services.collection.save(cmd, co) + } + } } diff --git a/src/services/cmdopts.ts b/src/services/cmdopts.ts index 489d9d5..c9d0e8f 100644 --- a/src/services/cmdopts.ts +++ b/src/services/cmdopts.ts @@ -1,193 +1,190 @@ import fs from 'fs-extra' import services from '@src/services/index.js' -import { Command } from 'commander' +import {Command} from 'commander' import * as uuid from 'uuid' -import psdk, { Collection } from 'postman-collection' +import psdk, {Collection} from 'postman-collection' import axios from 'axios' import path from 'node:path' export class CmdOptsService { - getOptVariables(cmd: Command) { - if (!cmd.parent) throw Error('cmd.parent is null') - - const variables = - cmd.parent.opts().variables || services.env.variables || '{}' - return JSON.parse(variables) - } - - #isCollectionId(id: string) { - // id may begin with an integer - const firstPart: string = id.split('-')[0] - const num = Number(firstPart) - if (Number.isInteger(num)) id = id.slice(firstPart.length + 1) - - if (path.extname(id).length) return false - - try { - const u = new URL(id) - return false - } catch (err) {} - - return uuid.validate(id) - } - - #isCollectionReadonlyUrl(url: string) { - let u: URL - try { - u = new URL(url) - } catch (err) { - return false - } - const accessKey = u.searchParams.get('access_key') - if (!accessKey) return false - if (!accessKey.startsWith('PMAT')) return false - if (u.host !== 'api.postman.com') return false - if (!u.pathname.startsWith('/collections/')) return false - - return true - } - - #isCollectionUrl(url: string) { - let u: URL - try { - u = new URL(url) - } catch (err) { - return false - } - - const accessKey = u.searchParams.get('access_key') - if (accessKey) return false - if (u.host !== 'api.postman.com') return false - if (!u.pathname.startsWith('/collections/')) return false - - return true - } - - async #isCollectionFile(path: string) { - const exists = await services.common.fileExists(path) - if (!exists) return false - - try { - await fs.readJson(path) - return true - } catch (err) { - return false - } - } - - async #getCollectionFetchHint(hint: string): Promise { - if (await this.#isCollectionFile(hint)) return 'file' - else if (this.#isCollectionReadonlyUrl(hint)) return 'url-readonly' - else if (this.#isCollectionUrl(hint)) return 'url' - else if (this.#isCollectionId(hint)) return 'id' - return 'none' - } - - async #fetchCollection(cmd: Command): Promise> { - if (!cmd.parent) throw Error('cmd.parent is null') - - // precedence order - const hintable = - cmd.parent.opts().collection || - services.env.collectionFilepath || - services.env.collectionUrl || - '' - const hint = await this.#getCollectionFetchHint(hintable) - - let collection: Record - - switch (hint) { - case 'none': - throw Error('no collection specified') - break - case 'file': { - const file = hintable - const co = await fs.readJson(file, 'utf8') - collection = co - if (co?.collection) collection = co.collection - break - } - case 'id': { - const id = hintable - const apikey = services.env.apiKey - const errApikey = - 'postman api key required, ' + - 'when collection id is specified' - if (!apikey) throw Error(errApikey) - - const opts = { headers: { 'X-API-Key': apikey } } - const url = 'https://api.getpostman.com/collections/' + id - try { - const { data } = await axios.get(url, opts) - collection = data.collection - } catch (err: any) { - throw Error('http request to fetch collection failed') - } - break - } - case 'url': { - const url = hintable - - const apikey = services.env.apiKey - const errApikey = - 'postman api key required, ' + - 'when collection url is specified' - if (!apikey) throw Error(errApikey) - - const opts = { headers: { 'X-API-Key': apikey } } - try { - const { data } = await axios.get(url, opts) - collection = data.collection - } catch (err: any) { - throw Error('http request to fetch collection failed') - } - break - } - case 'url-readonly': { - const url = hintable - - try { - const { data } = await axios.get(url) - collection = data.collection - } catch (err: any) { - const e = 'http request to fetch readonly collection failed' - throw Error(e) - } - break - } - } - - this.#cachedCollectionJson = collection - this.cachedCollectionHint = hint - this.cachedCollectionHintValue = hintable - return collection - } - - cachedCollectionHintValue = '' - cachedCollectionHint: CollectionHint = 'none' - #cachedCollectionJson: undefined | Record = undefined - - async getOptCollection(cmd: Command, forceFetch = false) { - let collection: Record = {} - const cache = this.#cachedCollectionJson - if (!cache || forceFetch) collection = await this.#fetchCollection(cmd) - else collection = cache - - const result = new psdk.Collection(collection) - return result - } - - getOptHeaders(cmd: Command): psdk.Header[] { - if (!cmd.parent) throw Error('cmd.parent is null') - const headersString = - cmd.parent.opts().headers || services.env.globalHeaders || '{}' - const headers = JSON.parse(headersString) - const result = Object.entries(headers).map(([k, v]) => { - const value: string | any = v - return new psdk.Header({ key: k, value, system: true }) - }) - return result - } + getOptVariables(cmd: Command) { + if (!cmd.parent) throw Error('cmd.parent is null') + + const variables = + cmd.parent.opts().variables || services.env.variables || '{}' + return JSON.parse(variables) + } + + #isCollectionId(id: string) { + // id may begin with an integer + const firstPart: string = id.split('-')[0] + const num = Number(firstPart) + if (Number.isInteger(num)) id = id.slice(firstPart.length + 1) + + if (path.extname(id).length) return false + + try { + const u = new URL(id) + return false + } catch (err) {} + + return uuid.validate(id) + } + + #isCollectionReadonlyUrl(url: string) { + let u: URL + try { + u = new URL(url) + } catch (err) { + return false + } + const accessKey = u.searchParams.get('access_key') + if (!accessKey) return false + if (!accessKey.startsWith('PMAT')) return false + if (u.host !== 'api.postman.com') return false + if (!u.pathname.startsWith('/collections/')) return false + + return true + } + + #isCollectionUrl(url: string) { + let u: URL + try { + u = new URL(url) + } catch (err) { + return false + } + + const accessKey = u.searchParams.get('access_key') + if (accessKey) return false + if (u.host !== 'api.postman.com') return false + if (!u.pathname.startsWith('/collections/')) return false + + return true + } + + async #isCollectionFile(path: string) { + const exists = await services.common.fileExists(path) + if (!exists) return false + + try { + await fs.readJson(path) + return true + } catch (err) { + return false + } + } + + async #getCollectionFetchHint(hint: string): Promise { + if (await this.#isCollectionFile(hint)) return 'file' + else if (this.#isCollectionReadonlyUrl(hint)) return 'url-readonly' + else if (this.#isCollectionUrl(hint)) return 'url' + else if (this.#isCollectionId(hint)) return 'id' + return 'none' + } + + async #fetchCollection(cmd: Command): Promise> { + if (!cmd.parent) throw Error('cmd.parent is null') + + // precedence order + const hintable = + cmd.parent.opts().collection || + services.env.collectionFilepath || + services.env.collectionUrl || + '' + const hint = await this.#getCollectionFetchHint(hintable) + + let collection: Record + + switch (hint) { + case 'none': + throw Error('no collection specified') + break + case 'file': { + const file = hintable + const co = await fs.readJson(file, 'utf8') + collection = co + if (co?.collection) collection = co.collection + break + } + case 'id': { + const id = hintable + const apikey = services.env.apiKey + const errApikey = + 'postman api key required, ' + + 'when collection id is specified' + if (!apikey) throw Error(errApikey) + + const opts = {headers: {'X-API-Key': apikey}} + const url = 'https://api.getpostman.com/collections/' + id + try { + const {data} = await axios.get(url, opts) + collection = data.collection + } catch (err: any) { + throw Error('http request to fetch collection failed') + } + break + } + case 'url': { + const url = hintable + + const apikey = services.env.apiKey + const errApikey = + 'postman api key required, ' + + 'when collection url is specified' + if (!apikey) throw Error(errApikey) + + const opts = {headers: {'X-API-Key': apikey}} + try { + const {data} = await axios.get(url, opts) + collection = data.collection + } catch (err: any) { + throw Error('http request to fetch collection failed') + } + break + } + case 'url-readonly': { + const url = hintable + + try { + const {data} = await axios.get(url) + collection = data.collection + } catch (err: any) { + const e = 'http request to fetch readonly collection failed' + throw Error(e) + } + break + } + } + + this.#cachedCollectionJson = collection + this.cachedCollectionHint = hint + this.cachedCollectionHintValue = hintable + return collection + } + + cachedCollectionHintValue = '' + cachedCollectionHint: CollectionHint = 'none' + #cachedCollectionJson: undefined | Record = undefined + + async getOptCollection(cmd: Command, forceFetch = false) { + let collection: Record = {} + const cache = this.#cachedCollectionJson + if (!cache || forceFetch) collection = await this.#fetchCollection(cmd) + else collection = cache + + const result = new psdk.Collection(collection) + return result + } + + getOptHeaders(cmd: Command): psdk.Header[] { + if (!cmd.parent) throw Error('cmd.parent is null') + const headersString = + cmd.parent.opts().headers || services.env.globalHeaders || '{}' + const headers = JSON.parse(headersString) + const result = services.common.jsonToHeaders(headers) + return result + } } type CollectionHint = 'file' | 'id' | 'url' | 'url-readonly' | 'none' diff --git a/src/services/common.ts b/src/services/common.ts index 89459f5..a2bf183 100644 --- a/src/services/common.ts +++ b/src/services/common.ts @@ -1,121 +1,136 @@ import fs from 'fs-extra' -import util, { inspect } from 'node:util' +import psdk from 'postman-collection' +import util, {inspect} from 'node:util' import lodash from 'lodash' -import newman, { NewmanRunOptions, NewmanRunSummary } from 'newman' +import newman, {NewmanRunOptions, NewmanRunSummary} from 'newman' import services from '@src/services/index.js' +import pretty from 'js-object-pretty-print' export class CommonService { - _ = lodash - - isIterable(value) { - return Symbol.iterator in Object(value) - } - - parseAxiosRes(err) { - const { - config: { url }, - response: { status, statusText, headers, data }, - } = err - return { url, status, statusText, headers, data } - } - - /** - * Promisified `newman.run`. - * `util.promisify()` does not work. - * @throws Error - */ - newmanRun(options: NewmanRunOptions): Promise { - const cb = (resolve, reject) => { - newman.run(options, (err, summary) => { - if (err) return reject(err) - resolve(summary) - }) - } - return new Promise(cb) - } - - /** - * Convert a string of JavaScript object - * into JSON-parsable string. - */ - toJsonString(input: string) { - const keyMatcher = '([^",{}\\s]+?)' - const valMatcher = '(.,*)' - const matcher = new RegExp(`${keyMatcher}\\s*:\\s*${valMatcher}`, 'g') - const parser = (_, key, value) => `"${key}":${value}` - return input.replace(matcher, parser) - } - - isJson(input: string) { - try { - JSON.parse(input) - return true - } catch (e) { - return false - } - } - - /** - * Pretty-prints an object recursively. - */ - getPrintedObject(o, opts: util.InspectOptions = {}): string { - const defaults = { - indentationLvl: 2, - colors: true, - depth: 5, - showHidden: false, - } - const result = inspect(o, { ...defaults, ...opts }) - return result - } - - getFormattedObject( - object: Record, - options?: Array | ({ ignore?: string[] } & util.InspectOptions) - ) { - const result: Record = {} - let ignore: string[] = [] - let inspectOpts: util.InspectOptions = {} - - if (!lodash.isArray(options)) { - if (options?.ignore) { - ignore.push(...options.ignore) - options = lodash.omit(options, 'ignore') - } - const { ..._inspectOpts } = options - inspectOpts = _inspectOpts - } else ignore = options - - Object.entries(object).forEach(([k, v]) => { - if (ignore.includes(k)) return - result[k] = v - }) - const printOpts = { ...inspectOpts } - return services.common.getPrintedObject(result, printOpts) - } - - benchSync(cb: () => unknown) { - const d = performance.now() - cb() - return performance.now() - d - } - - sleep(ms: number) { - return new Promise(r => setTimeout(r, ms)) - } - - fileExists(path) { - return fs - .access(path) - .then(() => true) - .catch(() => false) - } - - arrayMove(arr: any[], fromIndex: number, toIndex: number) { - const element = arr[fromIndex] - arr.splice(fromIndex, 1) - arr.splice(toIndex, 0, element) - } + _ = lodash + + isIterable(value) { + return Symbol.iterator in Object(value) + } + + parseAxiosRes(err) { + const { + config: {url}, + response: {status, statusText, headers, data}, + } = err + return {url, status, statusText, headers, data} + } + + /** + * Promisified `newman.run`. + * `util.promisify()` does not work. + * @throws Error + */ + newmanRun(options: NewmanRunOptions): Promise { + const cb = (resolve, reject) => { + newman.run(options, (err, summary) => { + if (err) return reject(err) + resolve(summary) + }) + } + return new Promise(cb) + } + + /** + * Convert a string of JavaScript object + * into JSON-parsable string. + */ + toJsonString(input: string) { + //const keyMatcher = '([^",{}\\s]+?)' + //const valMatcher = '(.,*)' + //const matcher = new RegExp(`${keyMatcher}\\s*:\\s*${valMatcher}`, 'g') + //const parser = (_, key, value) => `"${key}":${value}` + //return input.replace(matcher, parser) + + //return input.replace(/([\$\w]+)\s*:/g, function (_, $1) {return '"' + $1 + '":'}) + //.replace(/'([^']+)'/g, function (_, $1) {return '"' + $1 + '"'}) + + return pretty.pretty(input, 4, 'JSON') + } + + isJson(input: string) { + try { + JSON.parse(input) + return true + } catch (e) { + return false + } + } + + jsonToHeaders(jsonHeaders: Record) { + const result = Object.entries(jsonHeaders).map(([k, v]) => { + const value: string | any = v + return new psdk.Header({key: k, value}) + }) + return result + } + + /** + * Pretty-prints an object recursively. + */ + getPrintedObject(o, opts: util.InspectOptions = {}): string { + const defaults = { + indentationLvl: 2, + colors: true, + depth: 5, + showHidden: false, + } + const result = inspect(o, {...defaults, ...opts}) + return result + } + + getFormattedObject( + object: Record, + options?: Array | ({ignore?: string[]} & util.InspectOptions) + ) { + const result: Record = {} + let ignore: string[] = [] + let inspectOpts: util.InspectOptions = {} + + if (!lodash.isArray(options)) { + if (options?.ignore) { + ignore.push(...options.ignore) + options = lodash.omit(options, 'ignore') + } + const {..._inspectOpts} = options + inspectOpts = _inspectOpts + } else ignore = options + + Object.entries(object).forEach(([k, v]) => { + if (ignore.includes(k)) return + result[k] = v + }) + const printOpts = {...inspectOpts} + return services.common.getPrintedObject(result, printOpts) + } + + benchSync(cb: () => unknown) { + const d = performance.now() + cb() + return performance.now() - d + } + + sleep(ms: number) { + return new Promise(r => setTimeout(r, ms)) + } + + fileExists(path) { + return fs + .access(path) + .then(() => true) + .catch(() => false) + } + + arrayMove(arr: any[], fromIndex: number, toIndex: number) { + const element = arr[fromIndex] + arr.splice(fromIndex, 1) + arr.splice(toIndex, 0, element) + } } export default new CommonService() diff --git a/src/services/example.ts b/src/services/example.ts index 85a8627..e758055 100644 --- a/src/services/example.ts +++ b/src/services/example.ts @@ -1,48 +1,63 @@ -import { PostmanCli } from '@src/types.js' +import {PostmanCli} from '@src/types.js' import psdk from 'postman-collection' import services from '@src/services/index.js' +type ToPrintableOpts = { + addParsedBody?: true +} + /** * Mostly an abstraction over psdk.Response * for cmd-show. */ export class ExampleService { - toPrintable(r: psdk.Response): PostmanCli.ExamplePrintable { - let urlMethod = '' - let urlPath = '' - const resultResponse = services.response.toPrintable(r) - let resultRequest: undefined | PostmanCli.RequestPrintable - - // cmd-show - // postman examples usually are under a request - // so examples have both request body + response body - if (r.originalRequest) { - resultRequest = services.request.toPrintable(r.originalRequest) - urlMethod = r.originalRequest.method - urlPath = r.originalRequest.url.getPath() - } - - const item = r.parent() as psdk.Item - if ((!urlMethod || !urlPath) && item) { - urlMethod = item.request.method - urlPath = item.request.url.getPath() - } - - return { response: resultResponse, request: resultRequest } - } - - getPrintString(r: PostmanCli.ExamplePrintable) { - let req = '' - if (r.request) req = services.request.getPrintString(r.request) - const res = services.response.getPrintString(r.response) - return req + '\n\n\n\n' + res - } - - print(r: psdk.Response) { - const printable = this.toPrintable(r) - const string = this.getPrintString(printable) - services.logger.out(string) - } + declare ToPrintableOpts: ToPrintableOpts + + toPrintable(r: psdk.Response, opts: ToPrintableOpts = {}): PostmanCli.ExamplePrintable { + let urlMethod = '' + let urlPath = '' + const resultResponse = services.response.toPrintable(r) + let resultRequest: undefined | PostmanCli.RequestPrintable + + // cmd-show + // postman examples usually are under a request + // so examples have both request body + response body + if (r.originalRequest) { + resultRequest = services.request.toPrintable(r.originalRequest) + urlMethod = r.originalRequest.method + urlPath = r.originalRequest.url.getPath() + } + + const item = r.parent() as psdk.Item + if ((!urlMethod || !urlPath) && item) { + urlMethod = item.request.method + urlPath = item.request.url.getPath() + } + + if (opts.addParsedBody && resultRequest?.$parsedBody) { + resultRequest.body = resultRequest.$parsedBody + delete resultRequest.$parsedBody + } + if (opts.addParsedBody && resultResponse?.$parsedBody) { + resultResponse.body = resultResponse.$parsedBody + delete resultResponse.$parsedBody + } + + return {response: resultResponse, request: resultRequest} + } + + getPrintString(r: PostmanCli.ExamplePrintable) { + let req = '' + if (r.request) req = services.request.getPrintString(r.request) + const res = services.response.getPrintString(r.response) + return req + '\n\n' + res + } + + print(r: psdk.Response) { + const printable = this.toPrintable(r) + const string = this.getPrintString(printable) + services.logger.out(string) + } } export default new ExampleService() diff --git a/src/services/response.ts b/src/services/response.ts index de26b22..fdced9a 100644 --- a/src/services/response.ts +++ b/src/services/response.ts @@ -3,157 +3,157 @@ import prettyBytes from 'pretty-bytes' import contentType from 'content-type' import psdk from 'postman-collection' import services from '@src/services/index.js' -import { PostmanCli } from '@src/types' +import {PostmanCli} from '@src/types' export class ResponseService { - isResponse(value): value is psdk.Response { - return psdk.Response.isResponse(value) - } - - getIcon() { - return chalk.italic.magenta(' res ') - } - - getCodeIcon(code: number, status: string) { - if (!code || !status) return '' - - let color = chalk - switch (code.toString()[0]) { - case '2': - color = chalk.green - break - case '4': - case '5': - color = chalk.red - break - default: - color = chalk.yellow - break - } - return color(code + ' ' + status) - } - - /** - * recursively prepared nested Enquirer choices, if - * json has nested objects. - */ - toFormChoices(json, { result, parentName }: any): void { - if (!parentName) parentName = '' - - if (services.common._.isPlainObject(json)) { - Object.entries(json).forEach(([k, v]) => { - if (services.common._.isPlainObject(v)) { - const nestedItem = { - name: k, - message: k, - type: 'form', - choices: [], - } - result.push(nestedItem) - return this.toFormChoices(v, { - parentName: k, - result: nestedItem.choices, - }) - } - const sep = '⁣' - const item = { - name: parentName ? parentName + sep + k : k, - message: k, - initial: v, - } - result.push(item) - }) - } - } - - /** - * Transforms response body to - */ - toEnquirerForm(r: psdk.Response): Error | any[] { - const result: any[] = [] - - const body = r?.originalRequest?.body - if (!body || !r.originalRequest) return result - - const json = services.request.toJsonBody(r.originalRequest) - if (services.common._.isError(json)) return json - - this.toFormChoices(json, { result }) - return result - } - - toPrintable(r: psdk.Response): PostmanCli.ResponsePrintable { - const headers = r.headers.toObject() - let $parseHint: PostmanCli.ResponseParseHint = 'none' - let $parsedBody: any - let rawBody: any - let urlMethod = '' - let urlPath = '' - - //if (headers['content-type'] && r.stream) { - // before cmd-run, and after cmd-run - if (r.body || r.stream) { - try { - $parsedBody = JSON.parse(r.body || r.stream?.toString() || '{}') - $parseHint = 'json' - } catch (e) { - const s = 'parsing only json request/response body is supported' - throw Error(s) - } - } - - if (r.originalRequest) { - urlMethod = r.originalRequest.method - urlPath = r.originalRequest.url.getPath() - } - - const parent = r.parent() as psdk.Item // this is undefined while in cmd-run! - if ((!urlMethod || !urlPath) && parent) { - // cmd-show - urlMethod = parent.request.method - urlPath = parent.request.url.getPath() - } - - return { - url: { - method: urlMethod, - path: urlPath, - }, - headers, - body: rawBody, - $parsedBody, - $parseHint, - size: r.size() as any, - time: r.responseTime, - code: r.code, - status: r.status, - } - } - - getPrintString(r: PostmanCli.ResponsePrintable): string { - const rr = Object.assign({}, r) - - const avail = ['json', 'text'].includes(rr.$parseHint || '') - if (avail) rr.body = rr.$parsedBody - - const opts = ['$parseHint', '$parsedBody', 'code', 'status', 'size'] - if (!rr.url.method || !rr.url.path) opts.push('url') // in cmd-run - - let result = this.getCodeIcon(rr.code, rr.status) - result += ' ' + prettyBytes(rr.size.total) - if (rr.time) { - result += ' ' + rr.time + ' ms' - opts.push('time') - } - result += '\n' + services.common.getFormattedObject(rr, opts) - return result - } - - print(r: psdk.Response) { - const printable = this.toPrintable(r) - const printString = this.getPrintString(printable) - services.logger.out(printString) - } + isResponse(value): value is psdk.Response { + return psdk.Response.isResponse(value) + } + + getIcon() { + return chalk.italic.magenta(' res ') + } + + getCodeIcon(code: number, status: string) { + if (!code || !status) return '' + + let color = chalk + switch (code.toString()[0]) { + case '2': + color = chalk.green + break + case '4': + case '5': + color = chalk.red + break + default: + color = chalk.yellow + break + } + return color(code + ' ' + status) + } + + /** + * recursively prepared nested Enquirer choices, if + * json has nested objects. + */ + toFormChoices(json, {result, parentName}: any): void { + if (!parentName) parentName = '' + + if (services.common._.isPlainObject(json)) { + Object.entries(json).forEach(([k, v]) => { + if (services.common._.isPlainObject(v)) { + const nestedItem = { + name: k, + message: k, + type: 'form', + choices: [], + } + result.push(nestedItem) + return this.toFormChoices(v, { + parentName: k, + result: nestedItem.choices, + }) + } + const sep = '⁣' + const item = { + name: parentName ? parentName + sep + k : k, + message: k, + initial: v, + } + result.push(item) + }) + } + } + + /** + * Transforms response body to + */ + toEnquirerForm(r: psdk.Response): Error | any[] { + const result: any[] = [] + + const body = r?.originalRequest?.body + if (!body || !r.originalRequest) return result + + const json = services.request.toJsonBody(r.originalRequest) + if (services.common._.isError(json)) return json + + this.toFormChoices(json, {result}) + return result + } + + toPrintable(r: psdk.Response): PostmanCli.ResponsePrintable { + const headers = r.headers.toObject() + let $parseHint: PostmanCli.ResponseParseHint = 'none' + let $parsedBody: any + let rawBody: any + let urlMethod = '' + let urlPath = '' + + //if (headers['content-type'] && r.stream) { + // before cmd-run, and after cmd-run + if (r.body || r.stream) { + try { + $parsedBody = JSON.parse(r.body || r.stream?.toString() || '{}') + $parseHint = 'json' + } catch (e) { + const s = 'parsing only json request/response body is supported' + throw Error(s) + } + } + + if (r.originalRequest) { + urlMethod = r.originalRequest.method + urlPath = r.originalRequest.url.getPath() + } + + const parent = r.parent() as psdk.Item // this is undefined while in cmd-run! + if ((!urlMethod || !urlPath) && parent) { + // cmd-show + urlMethod = parent.request.method + urlPath = parent.request.url.getPath() + } + + return { + url: { + method: urlMethod, + path: urlPath, + }, + headers, + body: rawBody, + size: r.size() as any, + time: r.responseTime, + code: r.code, + status: r.status, + $parsedBody, + $parseHint, + } + } + + getPrintString(r: PostmanCli.ResponsePrintable): string { + const rr = Object.assign({}, r) + + const avail = ['json', 'text'].includes(rr.$parseHint || '') + if (avail) rr.body = rr.$parsedBody + + const opts = ['$parseHint', '$parsedBody', 'code', 'status', 'size'] + if (!rr.url.method || !rr.url.path) opts.push('url') // in cmd-run + + let result = this.getCodeIcon(rr.code, rr.status) + result += ' ' + prettyBytes(rr.size.total) + if (rr.time) { + result += ' ' + rr.time + ' ms' + opts.push('time') + } + result += '\n' + services.common.getFormattedObject(rr, opts) + return result + } + + print(r: psdk.Response) { + const printable = this.toPrintable(r) + const printString = this.getPrintString(printable) + services.logger.out(printString) + } } export default new ResponseService()