From ba1d39e6d38edfaa1376d2ec152287f14f601333 Mon Sep 17 00:00:00 2001 From: Patrick Michalina Date: Tue, 29 Jan 2019 14:40:07 -0600 Subject: [PATCH] fix: xml-js parses raw string instead of DOMParser object. (#10) BREAKING CHANGE: result object now includes json and xml output. --- README.md | 18 +++----- src/soap/request.ts | 100 ++++++++++++++++++++++---------------------- 2 files changed, 56 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 257161f..9d57b06 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ For more information about the TypeScript API checkout the [generated API docs]( - [x] Generate API with typings and docs from WSDL's and XSD's - [x] Execute simple (parameter-less) requests -- [ ] Execute requests with paremeters +- [x] Execute requests with paremeters ## Node Installation This package is designed to be run in both the browser and node environments. @@ -88,13 +88,10 @@ device.api.Device.GetUsers() .toPromise() .then(res=> { res.match({ // results are wrapped in a managed object for safer processing - ok: console.log, // successful response object - fail: r => console.log(r.status, r.statusMessage) // request failure object + ok: success => console.log(success.json), // successful response object + fail: railure => console.log(railure.status, railure.statusMessage) // request failure object }) }) - -// output -// { User: { Username: 'admin', UserLevel: 'Administrator' } } ``` ### Ad Hoc Usage @@ -106,7 +103,7 @@ Device.GetUsers() .run({ system: DEFAULT_NODE_ENV, deviceUrl: 'http://192.168.1.11/onvif/device_service', - user: maybe({ // currenlty requires a wrapper object, will improve in the future + user: maybe({ // currently requires a wrapper object, will improve in the future username: 'admin', password: '1234' }) @@ -114,11 +111,8 @@ Device.GetUsers() .toPromise() .then(res=> { res.match({ - ok: console.log, // successful response object - fail: r => console.log(r.status, r.statusMessage) // request failure object + ok: success => console.log(success.json), // successful response object + fail: railure => console.log(railure.status, railure.statusMessage) // request failure object }) }) - -// output -// { User: { Username: 'admin', UserLevel: 'Administrator' } } ``` diff --git a/src/soap/request.ts b/src/soap/request.ts index 7934983..86493e5 100644 --- a/src/soap/request.ts +++ b/src/soap/request.ts @@ -5,6 +5,13 @@ import { map } from 'rxjs/operators' import { createUserToken } from './auth' import { xml2json } from 'xml-js' +export interface IResultStructure { + readonly json: T + readonly xml: string +} + +export type IOnvifResult = IResult + export enum XMLNS { S11 = 'xmlns:S11="http://www.w3.org/2003/05/soap-envelope"', wsse = 'xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"', @@ -41,23 +48,12 @@ export enum XMLNS { wsa5 = 'xmlns:wsa5="http://www.w3.org/2005/08/addressing"' } -export interface ITransportPayloadXml { - readonly body: Document - readonly statusMessage: string - readonly status: number -} - -export type IOnvifNetworkResponse = Observable> - const parseXml = (parser: DOMParser) => - (payload: ITransportPayoad): ITransportPayloadXml => { - // console.log(payload.body) - return { - ...payload, - body: parser.parseFromString(payload.body, 'text/xml') - } - } + (xmlString: string): Document => + parser.parseFromString(xmlString, 'text/xml') + +const nsstr = () => Object.keys(XMLNS).map((k: any) => XMLNS[k]) export enum SOAP_NODE { Header = 'S11:Header', @@ -65,8 +61,6 @@ export enum SOAP_NODE { Body = 'S11:Body' } -const nsstr = () => Object.keys(XMLNS).map((k: any) => XMLNS[k]) - export const soapShell = (rawBody: string) => (rawHeader?: string) => @@ -80,23 +74,26 @@ export const mapResponseXmlToJson = (node: string) => (source: Observable) => source.pipe( - map(a => a.map(b => { + map(a => a.map>(b => { const split = node.split(':') const nodeKey = maybe(split[1]).valueOr(node) - const soapNsPrefix = b.documentElement.lookupPrefix(b.documentElement.namespaceURI) + const soapNsPrefix = b.xmlDocument.documentElement.lookupPrefix(b.xmlDocument.documentElement.namespaceURI) - const parsed = JSON.parse(xml2json(b as any, { + const parsed = JSON.parse(xml2json(b.xmlString, { compact: true, spaces: 2, ignoreAttributes: true, elementNameFn: d => maybe(d.split(':')[1]).valueOr(d) })) - const resultObject = parsed['Envelope'] + const json = parsed['Envelope'] ? parsed['Envelope']['Body'][nodeKey] : parsed[`${soapNsPrefix}:Envelope`][`${soapNsPrefix}:Body`][nodeKey] - return resultObject as T + return { + json, + xml: b.xmlString + } }))) export const mapResponseObsToProperty = @@ -104,21 +101,46 @@ export const mapResponseObsToProperty = (source: Observable>) => source.pipe(map(a => a.map(propSelectFn))) -type IOnvifResult = IResult +export interface IXmlContainer { + readonly xmlString: string + readonly xmlDocument: Document +} + +// tslint:disable-next-line:readonly-array +export const generateRequestElements = (reqNode: string) => (parameterNodes: string[]) => (...params: any[]) => { + return !params.length + ? `<${reqNode} />` + : `<${reqNode}>${params.map((param, index) => { + const type = typeof param + const insertIntoRootNode = (inner: string) => `<${parameterNodes[index]}>${inner}` + + switch (type) { + case 'undefined': return '' + case 'boolean': return insertIntoRootNode(param) + case 'object': return insertIntoRootNode(Object.keys(param).reduce((acc, key) => { + const val = (param as any)[key] + return val + ? (acc || '') + `<${key}>${(param as any)[key]}` + : (acc || '') + }, '')) + default: return insertIntoRootNode(param) + } + }).join('')}` +} export const createStandardRequestBody = (body: string) => reader>(config => { const gen = (body: string) => config.system.transport(body)(config.deviceUrl) - .pipe(map(parseXml(config.system.parser))) .pipe(map(response => { + const xmlDocument = parseXml(config.system.parser)(response.body) const tmp = (XMLNS.S11.split('=').pop() || '').replace(/"/g, '') - const subcode = maybe(response.body.getElementsByTagNameNS(tmp, 'Subcode').item(0)).flatMapAuto(a => a.textContent) - const reason = maybe(response.body.getElementsByTagNameNS(tmp, 'Reason').item(0)).flatMapAuto(a => a.textContent) + const subcode = maybe(xmlDocument.getElementsByTagNameNS(tmp, 'Subcode').item(0)).flatMapAuto(a => a.textContent) + const reason = maybe(xmlDocument.getElementsByTagNameNS(tmp, 'Reason').item(0)).flatMapAuto(a => a.textContent) return response.status === 200 && !reason.valueOrUndefined() - ? ok(response.body) - : fail({ + ? ok({ xmlString: response.body, xmlDocument }) + : fail({ ...response, statusMessage: (reason.valueOrUndefined() || subcode.valueOr(response.statusMessage)).trim() }) @@ -146,25 +168,3 @@ export const createDeviceRequestBodyFromString = export const createMediaRequestBodyFromString = (key: string) => createSimpleRequestBodyFromString(`trt:${key}`) - -// tslint:disable-next-line:readonly-array -export const generateRequestElements = (reqNode: string) => (parameterNodes: string[]) => (...params: any[]) => { - return !params.length - ? `<${reqNode} />` - : `<${reqNode}>${params.map((param, index) => { - const type = typeof param - const insertIntoRootNode = (inner: string) => `<${parameterNodes[index]}>${inner}` - - switch (type) { - case 'undefined': return '' - case 'boolean': return insertIntoRootNode(param) - case 'object': return insertIntoRootNode(Object.keys(param).reduce((acc, key) => { - const val = (param as any)[key] - return val - ? (acc || '') + `<${key}>${(param as any)[key]}` - : (acc || '') - }, '')) - default: return insertIntoRootNode(param) - } - }).join('')}` -}