diff --git a/src/commands/1.0,1.1/get.ts b/src/commands/1.0,1.1/get.ts index 22aa687a190f0bc8e6d9e77baf5d8937f00fad13..c64ecf32c1848c6b785f819f7bd5c8869f93bf89 100644 --- a/src/commands/1.0,1.1/get.ts +++ b/src/commands/1.0,1.1/get.ts @@ -28,10 +28,15 @@ const decode: DecodeFn = (msg, decodeOptions) => { pathIncludesSearch || hasMultipleIndexes || resolvedPathResultsArr.length > 1; - const unflattened = util.convertToNestedObject(resolvedPathResultsArr); - const unwrapped = util.fullyUnwrapObject(unflattened, shouldBeArray); - return [util.isEmptyObject(unwrapped) ? [] : unwrapped]; + return [ + util.processGetResult( + resolvedPathResultsArr, + shouldBeArray, + requestedPath, + decodeOptions + ), + ]; } return [null]; }; @@ -49,7 +54,9 @@ const encode: EncodeFn = ({ paths, options }) => ({ lookup: "Request", get: { paramPaths: Array.isArray(paths) ? paths : [paths], - ...(Number.isInteger(options?.max_depth) ? { maxDepth: options.max_depth } : {}) + ...(Number.isInteger(options?.max_depth) + ? { maxDepth: options.max_depth } + : {}), }, }, }, diff --git a/src/commands/1.2/get.ts b/src/commands/1.2/get.ts index 297816cf7d4f7a2b7266ac39e05f77c90ddab6b5..13caa65acfe9b294df967c177be34920c7dbe940 100644 --- a/src/commands/1.2/get.ts +++ b/src/commands/1.2/get.ts @@ -13,14 +13,22 @@ const decode: DecodeFn = (msg, decodeOptions) => { (path) => path.includes("*") || path.includes("[") ); const hasMultipleRequestedPaths = requestedPaths.length > 1; - const resultIsArray = requestedPaths.some((reqPath) => resolvedPaths.some(path => util.isDigit(path.replace(reqPath, "").replace(".","")))); + const resultIsArray = requestedPaths.some((reqPath) => + resolvedPaths.some((path) => + util.isDigit(path.replace(reqPath, "").replace(".", "")) + ) + ); const shouldBeArray = pathIncludesSearch || hasMultipleRequestedPaths || resultIsArray; - - const unflattened = util.convertToNestedObject(resolvedPathResultsArr); - const unwrapped = util.fullyUnwrapObject(unflattened, shouldBeArray); - return [util.isEmptyObject(unwrapped) ? [] : unwrapped]; + return [ + util.processGetResult( + resolvedPathResultsArr, + shouldBeArray, + requestedPaths[0], + decodeOptions, + ), + ]; } return [null]; }; diff --git a/src/commands/util.ts b/src/commands/util.ts index ed048a54993f5822fe914667e83f532152631385..2f643dfd3ec647a2253082e9ed52059685699aef 100644 --- a/src/commands/util.ts +++ b/src/commands/util.ts @@ -1,4 +1,4 @@ -import { CommandType } from "../types"; +import { CommandType, DecodeOptions } from "../types"; // import set from "lodash.set"; const digitDotRe = /^\d+\..*$/; @@ -72,7 +72,7 @@ const fixPath = (s: string): string => .split(".[") .join("["); -export const convertToNestedObject = (arr) => { +export const convertToNestedObject = (arr: any[], retainPath?: boolean) => { const res = {}; arr .map((it) => @@ -205,3 +205,56 @@ export const parseID = (msg: any) => { : search(msg, "subscriptionId") || null; return id; }; + +const addPaths = (arr: any[]) => + arr.map(({ resolvedPath, resultParams }) => ({ + resolvedPath, + resultParams: { ...resultParams, query: resolvedPath }, + })); + +type RetainPathGetResult = { + query: string; + result: string | Record<string, string> | RetainPathGetResult[]; +}; + +const getOther = (obj: Record<string, string>, skipKey: string) => + obj[Object.keys(obj).find((key) => key !== skipKey) || ""]; + +const skipKey = (obj: Record<string, string>, keyToSkip: string) => + Object.entries(obj).reduce( + (acc, [key, val]) => (key !== keyToSkip ? { ...acc, [key]: val } : acc), + {} + ); + +const isValue = (obj: Record<string, string>) => Object.keys(obj).length === 2; + +const nestResult = ( + result: any, + query?: string +): RetainPathGetResult => ({ + query: query || result.query, + result: Array.isArray(result) + ? result.map((v) => nestResult(v)) + : isValue(result) + ? getOther(result, "query") + : skipKey(result, "query"), +}); + +export const processGetResult = ( + resolvedPathResultsArr: any[], + shouldBeArray: boolean, + requestedPath: string, + decodeOptions?: DecodeOptions, +) => { + const retainPath = decodeOptions?.retainPath === true; + const resolvedValues = retainPath + ? addPaths(resolvedPathResultsArr) + : resolvedPathResultsArr; + const unflattened = convertToNestedObject(resolvedValues); + const unwrapped = fullyUnwrapObject(unflattened, shouldBeArray); + const result = isEmptyObject(unwrapped) ? [] : unwrapped; + const modifiedResult = retainPath + ? nestResult(result, requestedPath) + : result; + return modifiedResult; +}; diff --git a/src/configurations/build.ts b/src/configurations/build.ts index a71618947dc79caee09bd8855a0be0f325a2388c..af822cd7299903be5001175e2ad1fade6606527f 100644 --- a/src/configurations/build.ts +++ b/src/configurations/build.ts @@ -49,6 +49,15 @@ const timeoutWrapper = (promise: () => Promise<any>, ms?: number) => { } else return promise(); }; +const applyCommandChanges = <T extends (...args: any[]) => any>(opts: Options, cmdName: keyof USP, cmd: T, args: any[]) => { + if (opts.get?.retainPath === true && cmdName === "get") { + const newOptions = args.length > 1 ? "retainPath" in args[1] ? args[1] : { retainPath: true, ...args[1] } : {}; + return cmd(...[args[0], newOptions, ...args.slice(2)]) + } + + return cmd(...args); +} + const wrap = <T extends (...args: any[]) => any>( opts: Options, cmdName: keyof USP, @@ -56,7 +65,7 @@ const wrap = <T extends (...args: any[]) => any>( ): T => { return <T>((...args: any[]) => { // Promise.resolve added to handle non-promise commands - const cmdCall = () => Promise.resolve(cmd(...args)); + const cmdCall = () => Promise.resolve(applyCommandChanges(opts, cmdName, cmd, args)); const finalCall = () => timeoutWrapper(cmdCall, opts.timeout) .then((res) => { diff --git a/src/types.ts b/src/types.ts index e6360be253a138e5348ceda212daaa49af7f9960..7bfaf6b15a740fce21c15be9a3b612e8c28c04e6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,10 @@ export type GetCommandOptions = { * Defaults to 2 (Only applies to usp version 1.2 ) */ max_depth?: number; + /** + * All results will be nestled into { result: <result>, query: <full path> } (defaults to false) + */ + retainPath?: boolean; }; export type GetReturn = string | Record<string, any> | Record<string, any>[]; @@ -222,6 +226,12 @@ export type Options = { preCall?: PreCallCallback; /** Call after before command */ postCall?: PostCallCallback; + get?: { + /** + * Set the retainPath option to all get calls + */ + retainPath?: boolean; + }; }; /** Device API */ @@ -342,7 +352,7 @@ export interface USP { * await usp.supportedProto() * ``` */ - supportedProto: SupportedProtoCommand; + supportedProto: SupportedProtoCommand; /** * Get instances @@ -500,8 +510,11 @@ export type ConnectionOptions = URLConnectionOptions | HostConnectionOptions; export type Response = string | Record<string, any>; +export type CommandOptions = GetCommandOptions; + export type DecodeOptions = { raw?: boolean; + retainPath?: boolean; }; export type DecodeFn = ( diff --git a/tests/integration/index.test.ts b/tests/integration/index.test.ts index 45d0af8ce981e496abbe8e28cc9b38ded022f7af..be8e14df933f7705b6656cdaec26f4cabe8cf45f 100644 --- a/tests/integration/index.test.ts +++ b/tests/integration/index.test.ts @@ -3,13 +3,23 @@ import connect from "../../src"; import { ConnectionOptions } from "../../src/types"; import config from "./config.json"; -describe("Test general API", function(){ +describe("Test general API", function () { this.timeout(10000); let device: any = null; + let added: string[] = []; before(async () => { device = await connect(config.ws as ConnectionOptions); + added = [ + await device.add("Device.NAT.PortMapping."), + await device.add("Device.NAT.PortMapping."), + ]; + }); + + after(async () => { + await device.del(added); + await device.disconnect(); }); // GET @@ -36,6 +46,58 @@ describe("Test general API", function(){ .catch((err) => assert.strictEqual(typeof err, "object")); }); + it("get a value with retainPath returns value with path", async () => { + const resp = await device.get("Device.NAT.PortMapping.1.Alias", { + retainPath: true, + }); + assert.strictEqual( + typeof resp.result === "string" && + resp.query === "Device.NAT.PortMapping.1.Alias", + true + ); + }); + + it("get an array of values with retainPath returns values with paths", async () => { + const resp = await device.get("Device.NAT.PortMapping.*.Alias", { + retainPath: true, + }); + assert.strictEqual( + typeof resp.result === "object" && + resp.query === "Device.NAT.PortMapping.*.Alias" && + Array.isArray(resp.result) && + resp.result.every( + (v) => "query" in v && "result" in v && typeof v.result === "string" + ), + true + ); + }); + + it("get an object with retainPath returns object with path", async () => { + const resp = await device.get("Device.NAT.PortMapping.1.", { + retainPath: true, + }); + assert.strictEqual( + typeof resp.result === "object" && + resp.query === "Device.NAT.PortMapping.1.", + true + ); + }); + + it("get an array of object with retainPath returns all objects with paths", async () => { + const resp = await device.get("Device.NAT.PortMapping.", { + retainPath: true, + }); + assert.strictEqual( + typeof resp.result === "object" && + resp.query === "Device.NAT.PortMapping." && + Array.isArray(resp.result) && + resp.result.every( + (v) => "query" in v && "result" in v && typeof v.result === "object" + ), + true + ); + }); + // SET it("set with value", async () => { @@ -90,7 +152,7 @@ describe("Test general API", function(){ // RESOLVE it("resolves references in object", async () => { - const msg = await device.resolve({ LowerLayers: "Device.Time." }) + const msg = await device.resolve({ LowerLayers: "Device.Time." }); assert.strictEqual(typeof msg.LowerLayers, "object"); }); @@ -230,13 +292,9 @@ describe("Test general API", function(){ }; const res = await op(input); await clearOp(); - + assert.strictEqual(typeof res, "object"); }).timeout(10000); - - after(async () => { - await device.disconnect(); - }); }); describe.skip("Test specific connection types", () => {